2026-04-05

This commit is contained in:
gcch 2026-04-05 13:11:21 +02:00
commit 2971f5516d
62 changed files with 439 additions and 497 deletions

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
@ -18,8 +17,6 @@ use function add_action;
use function array_map;
use function wc_get_products;
require_once __DIR__ . '/src/inc/TraitementInformations.php';
$context = Timber::context();
$templates = ['boutique.twig'];
@ -32,12 +29,7 @@ $products = array_map(
);
$context['products'] = $products;
/**
* Charge les scripts et styles de la page.
*
* @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 {
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-boutique',
path: '/assets/css/pages/page-boutique.css',
@ -50,9 +42,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-menu-categories',
path: '/assets/js/scripts-menu-categories.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
Timber::render(
data: $context,

View file

@ -310,6 +310,7 @@ button.bouton-blanc-sur-noir {
}
button.bouton-retour-haut {
position: fixed;
z-index: 500;
right: var(--espace-xl);
bottom: calc(var(--espace-l) + var(--pied-de-page-hauteur));
transform: rotate(180deg);
@ -321,7 +322,6 @@ button.bouton-retour-haut {
background: var(--couleur-fond);
box-shadow: initial;
transition: 0.2s background, 0.2s opacity, 0.2s visibility;
z-index: 500;
}
button.bouton-retour-haut img {
width: 1rem;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -11,18 +11,18 @@ jQuery(document).ready(function ($) {
$(".customize-control-tinymce-editor").each(function () {
// Get the toolbar strings that were passed from the PHP Class
var tinyMCEToolbar1String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar1;
var tinyMCEToolbar2String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar2;
var tinyMCEMediaButtons = _wpCustomizeSettings.controls[$(this).attr("id")].skyrocketmediabuttons;
const tinyMCEToolbar1String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar1;
const tinyMCEToolbar2String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar2;
const tinyMCEMediaButtons = _wpCustomizeSettings.controls[$(this).attr("id")].skyrocketmediabuttons;
wp.editor.initialize($(this).attr("id"), {
mediaButtons: tinyMCEMediaButtons,
quicktags: true,
tinymce: {
wpautop: true,
toolbar1: tinyMCEToolbar1String,
toolbar2: tinyMCEToolbar2String,
},
quicktags: true,
mediaButtons: tinyMCEMediaButtons,
});
});
$(document).on("tinymce-editor-init", function (event, editor) {

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
@ -17,12 +16,7 @@ use function add_action;
$context = Timber::context();
$templates = ['accueil.twig'];
/**
* Charge les scripts et styles de la page.
*
* @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 {
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',
@ -31,9 +25,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-page-accueil',
path: '/assets/js/scripts-page-accueil.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
Timber::render(
data: $context,

View file

@ -29,12 +29,7 @@ if (is_bool($image_dimensions)) {
$context['image_dimensions'] = $image_dimensions;
/**
* Charge les scripts et styles de la page.
*
* @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 {
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',
@ -43,9 +38,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-page-a-propos',
path: '/assets/js/scripts-page-a-propos.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
Timber::render(
data: $context,

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\Data\Cart;
use HaikuAtelier\WP\Resource;
use Illuminate\Support\Number;
@ -99,12 +98,7 @@ $context['pays_livraison'] = $allowed_countries;
$context['sous_total_livraison'] = $shipping_subtotal;
$context['methodes_livraison'] = $methodes_livraison;
/**
* Charge les scripts et styles de la page.
*
* @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 {
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-panier',
path: '/assets/css/pages/page-panier.css',
@ -113,9 +107,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-page-panier',
path: '/assets/js/scripts-page-panier.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
@ -17,19 +16,12 @@ use function add_action;
$context = Timber::context();
$templates = ['contact.twig'];
/**
* Charge les scripts et styles de la page.
*
* @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 {
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-contact',
path: '/assets/css/pages/page-contact.css',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
@ -17,19 +16,12 @@ use function add_action;
$context = Timber::context();
$templates = ['echec-commande.twig'];
/**
* Charge les scripts et styles de la page.
*
* @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 {
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',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -8,26 +8,20 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
use function add_action;
$context = Timber::context();
$templates = ['cgv.twig'];
/**
* Charge les scripts et styles de la page.
*
* @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 {
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',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -20,7 +20,6 @@ use function add_action;
use function assert;
use function collect;
use function is_array;
use function is_bool;
use function recupere_produits_meme_collection;
use function wc_get_product;
use function wp_json_encode;
@ -56,12 +55,7 @@ $context['product_json'] = wp_json_encode($product);
$context['maximum_price'] = $maximum_price;
$context['same_collection_products'] = $same_collection_products;
/**
* Charge les scripts et styles de la page.
*
* @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 {
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-produit',
path: '/assets/js/scripts-page-produit.js',
@ -70,9 +64,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-menu-categories',
path: '/assets/js/scripts-menu-categories.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -8,6 +8,8 @@ use Illuminate\Support\Arr;
use WC_Product_Attribute;
use WP_Term;
use function wc_attribute_label;
final readonly class Attribute {
/**
* @param list<AttributeOption> $options

View file

@ -11,14 +11,17 @@ use Psl\Option;
use WC_Product;
use WP_Term;
use function HaikuAtelier\genere_balise_img_multiformats;
use function head;
use function Psl\Option\from_nullable;
use function wpautop;
final readonly class Product {
/**
* @param list<Attribute> $attributes
* @param list<string> $left_column_photos
* @param list<string> $right_column_photos
* @param list<ProductVariation> $variations
* @param array<ProductVariation> $variations
*/
private function __construct(
public array $attributes,
@ -43,11 +46,11 @@ final readonly class Product {
*/
public static function get_attributes_for_product(WC_Product $product): array {
/** @var list<Attribute> */
return $product->get_attributes() |> (static fn($attributes) => Arr::map($attributes, Attribute::new(...)));
return $product->get_attributes()
|> (static fn(array $attributes): array => Arr::map($attributes, Attribute::new(...)));
}
public static function new(WC_Product $product): self {
/** @var list<Attribute> */
$attributes = self::get_attributes_for_product($product);
/** @var lowercase-string */
$category = $product->get_id() |> wc_get_product_category_list(...) |> strtolower(...);
@ -74,9 +77,13 @@ final readonly class Product {
$hover_photo = $right_column_photos[0] ?? genere_balise_img_multiformats('-1', true);
$slug = $product->get_slug();
$stock = $product->get_stock_quantity() ?? 1;
/** @var array<ProductVariation> */
$variations = $product->get_children()
|> (static fn($ids) => Arr::map($ids, wc_get_product(...)))
|> (static fn($products) => Arr::map($products, ProductVariation::new(...)));
|> (static fn(/** @var list<int> */ array $ids): array => Arr::map($ids, wc_get_product(...)))
|> (static fn(/** @var list<WC_Product> */ array $products): array => Arr::map(
$products,
ProductVariation::new(...),
));
$url = $product->get_permalink();
return new self(

View file

@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
/**
* Définis les fonctionnalités du thème et personnalise certaines existantes.
*/
declare(strict_types=1);
namespace HaikuAtelier;
// Désactive divers transformations du contenu par WordPress
function desactive_wpautop(): void {
@ -41,7 +43,7 @@ function autorise_import_svg_mediatheque(array $file_types): array {
$new_filetypes = [];
$new_filetypes['svg'] = 'image/svg+xml';
return array_merge($file_types, $new_filetypes);
return [...$file_types, ...$new_filetypes];
}
function retire_motifs_blocs_gutenberg(): void {
@ -56,8 +58,8 @@ function retire_styles_core_block(): void {
add_filter('async_update_translation', '__return_false');
add_filter('auto_update_translation', '__return_false');
add_action('init', 'desactive_wpautop');
add_filter('tiny_mce_before_init', 'desactive_transformation_contenu_tinymce');
add_filter('upload_mimes', 'autorise_import_svg_mediatheque');
add_action('after_setup_theme', 'retire_motifs_blocs_gutenberg');
add_action('wp_footer', 'retire_styles_core_block', 5);
add_action('init', desactive_wpautop(...));
add_filter('tiny_mce_before_init', desactive_transformation_contenu_tinymce(...));
add_filter('upload_mimes', autorise_import_svg_mediatheque(...));
add_action('after_setup_theme', retire_motifs_blocs_gutenberg(...));
add_action('wp_footer', retire_styles_core_block(...), 5);

View file

@ -1,10 +1,15 @@
<?php
declare(strict_types=1);
/**
* Créé les Taxonomies associées aux Produits.
*/
declare(strict_types=1);
namespace HaikuAtelier;
use function add_action;
use function register_taxonomy;
/**
* Enregistre la Taxonomie « Collection ».
@ -37,4 +42,4 @@ function enregistre_taxonomie_collection(): void {
register_taxonomy('collection', ['product'], $args);
}
add_action('init', 'enregistre_taxonomie_collection');
add_action('init', enregistre_taxonomie_collection(...));

View file

@ -6,8 +6,7 @@ declare(strict_types=1);
* Fonctions pour le traitement d'informations.
*/
use HaikuAtelier\Data\Attribute;
use HaikuAtelier\Data\Product;
namespace HaikuAtelier;
use function Crell\fp\pipe;
@ -87,118 +86,8 @@ function genere_balise_img_multiformats(string $id, bool $lazy = false): string
EOD;
}
/**
* TODO.
*/
function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int {
return $b->get_price() <=> $a->get_price();
}
/**
* Récupère les informations utilisées pour la grille des Produits et les retourne sous forme
* de tableau associatif.
*
* @return mixed un tableau avec uniquement les informations pour la Grille de Produits
*/
function recupere_informations_produit_shop(WC_Product $produit): mixed {
/** @var int $prix_maximal Le prix maximal du Produit. */
$prix_maximal = pipe(
// Récupère les Variations
$produit->get_children(),
// Récupère les informations de chaque Variation
static fn($enfants): array => array_map(
callback: wc_get_product(...),
array: $enfants,
),
// Trie les Variations par prix descendant
static fn($variations): array => array_map(
callback: static fn($variation) => $variation->get_price(),
array: $variations,
),
// Récupère le Prix de la Variation la plus chère
static fn($prix) => collect($prix)->max(),
// Récupère le Prix pour la Variation la plus chère OU le prix du Produit simple
static fn($prix_variation_maximale) => $prix_variation_maximale ?? $produit->get_price(),
);
// TEMP: Cas de la Carte Cadeau où aucun prix ne doit être affiché. Idéalement utiliser un système d'étiquettes pour ces cas là.
if ($produit->get_sku() === 'GIFTcard') {
$prix_maximal = '';
}
return [
// Identifiant du Produit
'id' => $produit->get_id(),
// Nom affiché du Produit
'nom' => $produit->get_name(),
// Prix affiché du Produit
'prix' => "{$prix_maximal}",
// Photo du Produit affichée par défaut
'photo_repos' => genere_balise_img_multiformats(
get_post_meta($post_id = $produit->get_id(), $key = '_photos_colonne_gauche|||0|value')[0] ?? -1,
false,
),
// Photo du Produit affichée au survol de l'image
'photo_survol' => genere_balise_img_multiformats(
get_post_meta($post_id = $produit->get_id(), $key = '_photos_colonne_droite|||0|value')[0] ?? -1,
true,
),
// URL du Produit pour les liens vers celui-ci
'url' => $produit->get_permalink(),
];
}
// Page Produit
/**
* Retourne un tableau associatif des informations affichées sur la page Produit depuis les données brutes d'un Produit.
*/
function recupere_informations_produit_page_produit(WC_Product $product): mixed {
/** @var list<Attribute> */
$attributs = Product::get_attributes_for_product($product);
return [
// Attributs du Produit
'attributs' => $attributs,
// Catégorie du Produit
'categorie' => pipe($product->get_id(), wc_get_product_category_list(...), strtolower(...)),
// Slug de la Collection - Peut ne pas avoir été défini
'collection' => get_the_terms($product->get_id(), 'collection')[0]->slug ?? '',
// Détails (Description) du Produit
'details' => wpautop($product->get_description()),
// Identifiant du Produit
'id' => $product->get_id(),
// Nom affiché du Produit
'nom' => $product->get_name(),
// Prix affiché du Produit
'prix' => $product->get_price(),
'photos_colonne_gauche' => array_map(
callback: genere_balise_img_multiformats(...),
array: get_post_meta($post_id = $product->get_id(), $key = '_photos_colonne_gauche|||0|value'),
),
'photos_colonne_droite' => array_map(
callback: genere_balise_img_multiformats(...),
array: carbon_get_the_post_meta('photos_colonne_droite'),
),
'photo_repos' => genere_balise_img_multiformats(
get_post_meta($post_id = $product->get_id(), $key = '_photos_colonne_gauche|||0|value')[0] ?? -1,
false,
),
'photo_survol' => genere_balise_img_multiformats(
get_post_meta($post_id = $product->get_id(), $key = '_photos_colonne_droite|||0|value')[0] ?? -1,
true,
),
// Slug du Produit
'slug' => $product->get_slug(),
// Quantité de Produit en stock
'stock' => $product->get_stock_quantity() ?? 1,
// Variations du Produit
'variations_ids' => $product->get_children(),
// URL du Produit
'url' => $product->get_permalink(),
];
}
/**
* 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.

View file

@ -6,6 +6,8 @@ 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;

View file

@ -8,6 +8,8 @@ use Psl\Option;
use WP_Error;
use WP_Term;
use function get_post_meta;
use function get_the_terms;
use function is_array;
use function Psl\Option\none;
use function Psl\Option\some;

View file

@ -6,9 +6,17 @@ namespace HaikuAtelier\WP;
use Exception;
use function filemtime;
use function get_template_directory;
use function get_template_directory_uri;
use function is_bool;
use function wp_enqueue_script_module;
use function wp_enqueue_style;
final readonly class Resource {
/**
* @throws Exception Lève une `Exception` s'il est impossible d'obtenir les attributs du fichier au chemin passé en paramètre.
*/
public static function enqueue_script_module_file(string $path, string $id): void {
$file_uri = get_template_directory_uri() . $path;
@ -16,7 +24,7 @@ final readonly class Resource {
$file_mtime = filemtime($file_path);
if (is_bool($file_mtime)) {
throw new Exception("Could not get modification time of file: {$file_uri} ");
throw new Exception("Impossible de récupérer la date de modification du fichier : {$file_uri}.");
}
$version = (string) $file_mtime;
@ -29,6 +37,9 @@ final readonly class Resource {
);
}
/**
* @throws Exception Lève une `Exception` s'il est impossible d'obtenir les attributs du fichier au chemin passé en paramètre.
*/
public static function enqueue_style_file(string $path, string $handle): void {
$file_uri = get_template_directory_uri() . $path;
@ -36,7 +47,7 @@ final readonly class Resource {
$file_mtime = filemtime($file_path);
if (is_bool($file_mtime)) {
throw new Exception("Could not get modification time of file: {$file_uri} ");
throw new Exception("Impossible de récupérer la date de modification du fichier : {$file_uri}.");
}
$ver = (string) $file_mtime;

View file

@ -69,6 +69,7 @@ button {
&.bouton-retour-haut {
position: fixed;
z-index: 500;
right: var(--espace-xl);
bottom: calc(var(--espace-l) + var(--pied-de-page-hauteur));
transform: rotate(180deg);
@ -80,7 +81,6 @@ button {
background: var(--couleur-fond);
box-shadow: initial;
transition: 0.2s background, 0.2s opacity, 0.2s visibility;
z-index: 500;
img {
width: 1rem;

View file

@ -1,5 +1,5 @@
import { Option, pipe } from "effect";
import { Array as EffectArray } from "effect";
import { Array as FxArray } from "effect";
import { NonEmptyReadonlyArray } from "effect/Array";
import { getOptionOrThrowWithError } from "./utils";
@ -27,8 +27,8 @@ export const getAllSelectorFromParent =
pipe(
parent.querySelectorAll<E>(selector),
// Convertis NodeListOf en Array.
Array.from<E>,
(xs: Array<E>) => Option.liftPredicate(EffectArray.isNonEmptyReadonlyArray)(xs),
(xs: NodeListOf<E>) => Array.from<E>(xs),
(xs: Array<E>) => Option.liftPredicate(FxArray.isReadonlyArrayNonEmpty)(xs),
);
export const getAllSelectorFromDocument = <E extends Element = Element>(

View file

@ -1,4 +1,4 @@
import { pipe, Option } from "effect";
import { Option, pipe } from "effect";
export const getOptionOrThrowWithError =
(message: string) =>

View file

@ -3,18 +3,18 @@
import { ATTRIBUT_CHARGEMENT } from "../constantes/dom";
// Types
interface AnimationCycleTexte {
type AnimationCycleTexte = {
callback: () => void;
etapes: Array<string>;
index: number;
interval: NodeJS.Timeout;
}
};
interface ParametresAnimationCycleTexte {
type ParametresAnimationCycleTexte = {
attribut: string;
element: HTMLElement;
etapes: Array<string>;
}
};
/**
* Créer le nécessaire pour une animation s'exécutant jusqu'à ce que un interval soit manuellement arrêté. L'animation
@ -39,7 +39,7 @@ const lanceAnimationCycleTexte = (args: ParametresAnimationCycleTexte): Animatio
},
etapes: args.etapes,
index: 0,
interval: setInterval(() => {}, 2147483647),
interval: setInterval(() => {}, 2_147_483_647),
};
return animation;

View file

@ -123,8 +123,12 @@ export const reporteEtLeveErreur = <E extends Error>(erreur: E): never => {
export const reporteEtJournaliseErreur = <E extends Error>(erreur: E): void => {
reporteErreur(erreur);
console.error(erreur);
if (erreur instanceof ValiError) console.error(erreur.issues);
if (erreur instanceof ErreurAdresseInvalide) console.error(erreur.problemes);
if (erreur instanceof ValiError) {
console.error(erreur.issues);
}
if (erreur instanceof ErreurAdresseInvalide) {
console.error(erreur.problemes);
}
};
export const reporteEtRetourneErreur = <E extends Error>(erreur: E): E => {

View file

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

View file

@ -1,6 +1,7 @@
import { pipe } from "@mobily/ts-belt";
import { Either } from "purify-ts";
import { parse, type ValiError } from "valibot";
import { parse } from "valibot";
import type { ValiError } from "valibot";
import type {
MessageMajBoutonPanier,

View file

@ -36,7 +36,7 @@ type ArgumentsPostBackendWC = {
route: string;
};
// fetch
// Fetch
export const getBackend = (args: ArgumentsGetBackendWC): Promise<Response> =>
fetch(args.route, {
@ -120,19 +120,16 @@ export const safeFetch = (f: Promise<Response>): EitherAsync<DOMException | Type
EitherAsync<DOMException | TypeError, Response>(async () => await f);
// Réponses Simplifiées
export const newPartialResponse = async (reponse: Response): Promise<SimplifiedResponse> => {
return {
body: await reponse.json(),
status: reponse.status,
};
};
export const newPartialResponse = async (reponse: Response): Promise<SimplifiedResponse> => ({
body: await reponse.json(),
status: reponse.status,
});
export const traiteErreursBackendWooCommerce = (rs: SimplifiedResponse): HttpCodeErrors => {
return match(rs)
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)));
};

View file

@ -6,7 +6,7 @@ export const WCStoreBillingAddressSchema = v.object({
city: v.string(),
company: v.string(),
country: v.string(),
// email: v.optional(v.pipe(v.string(), v.email())),
// Email: v.optional(v.pipe(v.string(), v.email())),
email: v.string(),
first_name: v.string(),
last_name: v.string(),

View file

@ -3,7 +3,8 @@ import type { GenericSchema, InferOutput, ValiError } from "valibot";
import { Either, Maybe } from "purify-ts";
import { safeJsonParse } from "./dom.ts";
import { ErreurEntreeInexistante, type NonExistingKeyError } from "./erreurs.ts";
import { ErreurEntreeInexistante } from "./erreurs.ts";
import type { NonExistingKeyError } from "./erreurs.ts";
import { safeSchemaParse, safeSchemaParseCurried } from "./validation.ts";
export type GetSessionStorage<S extends GenericSchema> = Either<ErreursGetSessionStorage<S>, InferOutput<S>>;

View file

@ -4,7 +4,7 @@ export type FetchErrors = DOMException | Error | TypeError;
export type HttpCodeErrors = BadRequestError | Error | ForbiddenError | NotFoundError | ServerError | UnauthorizedError;
export interface SimplifiedResponse {
export type SimplifiedResponse = {
body: unknown;
status: number;
}
};

View file

@ -1,5 +1,6 @@
import { D } from "@mobily/ts-belt";
import { type Either, Maybe } from "purify-ts";
import { Maybe } from "purify-ts";
import type { Either } from "purify-ts";
import { CleNonTrouveError } from "./erreurs";

View file

@ -3,7 +3,8 @@
*/
import { Either } from "purify-ts";
import { type GenericSchema, type InferOutput, parse, type ValiError } from "valibot";
import { parse } from "valibot";
import type { GenericSchema, InferOutput, ValiError } from "valibot";
export const safeSchemaParse = <Schema extends GenericSchema>(
valeur: unknown,

View file

@ -4,7 +4,8 @@ import { map as dictMap, values as dictValues } from "@mobily/ts-belt/Dict";
import { trim as stringTrim } from "@mobily/ts-belt/String";
import { EitherAsync, Maybe } from "purify-ts";
import { match, P } from "ts-pattern";
import { type AnySchema, ValiError } from "valibot";
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";
@ -43,10 +44,10 @@ import { safeSchemaParse } from "../lib/validation";
import { E } from "./scripts-page-panier-elements";
import { getShippingRatesLS } from "./scripts-page-panier-local-storage";
interface Addresses {
type Addresses = {
billing_address: WCStoreBillingAddress;
shipping_address: WCStoreShippingAddress;
}
};
// @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
@ -65,35 +66,33 @@ export const initCartFormEventEmitters = (): void => {
});
};
export const getAddressesFromForm = (formFields: Record<string, string>, areAddressesMerged: boolean): Addresses => {
return {
billing_address: {
address_1: formFields["facturation-adresse"] ?? formFields["livraison-adresse"] ?? "",
address_2: "",
city: formFields["facturation-ville"] ?? formFields["livraison-ville"] ?? "",
company: "",
country: areAddressesMerged ? (formFields["facturation-pays"] ?? "") : (formFields["livraison-pays"] ?? ""),
email: formFields["facturation-email"] ?? formFields["livraison-email"] ?? "",
first_name: formFields["facturation-prenom"] ?? formFields["livraison-prenom"] ?? "",
last_name: formFields["facturation-nom"] ?? formFields["livraison-nom"] ?? "",
phone: formFields["facturation-telephone"] ?? formFields["livraison-telephone"] ?? "",
postcode: formFields["facturation-code-postal"] ?? formFields["livraison-code-postal"] ?? "",
state: formFields["facturation-region-etat"] ?? formFields["livraison-region-etat"] ?? "",
},
shipping_address: {
address_1: formFields["livraison-adresse"] ?? "",
address_2: "",
city: formFields["livraison-ville"] ?? "",
company: "",
country: formFields["livraison-pays"] ?? "",
first_name: formFields["livraison-prenom"] ?? "",
last_name: formFields["livraison-nom"] ?? "",
phone: formFields["livraison-telephone"] ?? "",
postcode: formFields["livraison-code-postal"] ?? "",
state: formFields["livraison-region-etat"] ?? "",
},
};
};
export const getAddressesFromForm = (formFields: Record<string, string>, areAddressesMerged: boolean): Addresses => ({
billing_address: {
address_1: formFields["facturation-adresse"] ?? formFields["livraison-adresse"] ?? "",
address_2: "",
city: formFields["facturation-ville"] ?? formFields["livraison-ville"] ?? "",
company: "",
country: areAddressesMerged ? (formFields["facturation-pays"] ?? "") : (formFields["livraison-pays"] ?? ""),
email: formFields["facturation-email"] ?? formFields["livraison-email"] ?? "",
first_name: formFields["facturation-prenom"] ?? formFields["livraison-prenom"] ?? "",
last_name: formFields["facturation-nom"] ?? formFields["livraison-nom"] ?? "",
phone: formFields["facturation-telephone"] ?? formFields["livraison-telephone"] ?? "",
postcode: formFields["facturation-code-postal"] ?? formFields["livraison-code-postal"] ?? "",
state: formFields["facturation-region-etat"] ?? formFields["livraison-region-etat"] ?? "",
},
shipping_address: {
address_1: formFields["livraison-adresse"] ?? "",
address_2: "",
city: formFields["livraison-ville"] ?? "",
company: "",
country: formFields["livraison-pays"] ?? "",
first_name: formFields["livraison-prenom"] ?? "",
last_name: formFields["livraison-nom"] ?? "",
phone: formFields["livraison-telephone"] ?? "",
postcode: formFields["livraison-code-postal"] ?? "",
state: formFields["livraison-region-etat"] ?? "",
},
});
export const initShippingCalculationButton = (): void => {
// Déclenche au clic sur le Bouton de soumission du Formulaire la requête pour le calcul des frais de livraison
@ -119,11 +118,11 @@ export const initShippingCalculationButton = (): void => {
void EitherAsync.liftEither(safeSchemaParse(formArgs, WCStoreCartUpdateCustomerArgsSchema))
// Désactive le Bouton pour empêcher des requêtes concurrentes
.ifRight((): void => setButtonLoadingState(E.BOUTON_ACTIONS_FORMULAIRE, true))
.chain((args: WCStoreCartUpdateCustomerArgs) => {
return safeFetch(postBackend(ROUTE_API_MAJ_CLIENT, JSON.stringify(args), false));
})
.chain((rs: Response) => {
return EitherAsync<ErreurAdresseInvalide | HttpCodeErrors, unknown>(
.chain((args: WCStoreCartUpdateCustomerArgs) =>
safeFetch(postBackend(ROUTE_API_MAJ_CLIENT, JSON.stringify(args), false)),
)
.chain((rs: Response) =>
EitherAsync<ErreurAdresseInvalide | HttpCodeErrors, unknown>(
async ({ throwE }): Promise<unknown> =>
match(await newPartialResponse(rs))
.with({ status: 200 }, (rs): unknown => rs.body)
@ -135,8 +134,8 @@ export const initShippingCalculationButton = (): void => {
(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 */
@ -299,9 +298,9 @@ export const initOrderCreationButton = (): void => {
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) => {
return safeFetch(postBackend(ROUTE_API_NOUVELLE_COMMANDES, JSON.stringify(args), true));
})
.chain((args: WCV3OrdersArgs) =>
safeFetch(postBackend(ROUTE_API_NOUVELLE_COMMANDES, JSON.stringify(args), true)),
)
.chain((rs: Response) =>
EitherAsync<HttpCodeErrors, unknown>(
async ({ throwE }): Promise<unknown> =>

View file

@ -130,7 +130,7 @@ export const initialiseElementsCodePromo = (): void => {
);
window.dispatchEvent(CODE_PROMO_MAJ_EVENT);
// emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
// EmetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
})
.ifLeft((erreur) => {
// Rétablis le texte d'origine
@ -194,7 +194,9 @@ export const initialiseElementsCodePromo = (): void => {
)
.chain((reponse: Response) =>
EitherAsync<ServerError, unknown>(async ({ throwE }) => {
if (estReponse500(reponse)) throwE(new ServerError("500 server Error"));
if (estReponse500(reponse)) {
throwE(new ServerError("500 server Error"));
}
return await reponse.json();
}),
)

View file

@ -1,5 +1,6 @@
import { forEach as arrayForEach, map as arrayMap } from "@mobily/ts-belt/Array";
import { html, render, type TemplateResult } from "lit-html";
import { html, render } from "lit-html";
import type { TemplateResult } from "lit-html";
import type { WCStoreCartTotals, WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreShippingRateShippingRates } from "../lib/types/api/couts-livraison";
@ -66,8 +67,9 @@ export const generateShippingRatesHTML = (
getDOMElementsWithSelector(container)("div[data-methode-initiale]").ifRight(arrayForEach((div) => div.remove()));
const selectedShippingRate: string = shippingRates.find((sr) => sr.selected)?.method_id ?? "";
const shippingRatesHTML: ReadonlyArray<TemplateResult> = arrayMap(shippingRates, (methode) => {
return html` <div>
const shippingRatesHTML: ReadonlyArray<TemplateResult> = arrayMap(
shippingRates,
(methode) => html` <div>
<input
id="methode-livraison-${methode.method_id}"
name="choix-methode-livraison"
@ -78,8 +80,8 @@ export const generateShippingRatesHTML = (
<label for="methode-livraison-${methode.method_id}"
>${methode.name} (${formateEnEuros(methode.price)})</label
>
</div>`;
});
</div>`,
);
// Ajoute les nouveaux Produits dans le DOM
container.removeAttribute(ATTRIBUT_HIDDEN);

View file

@ -4,7 +4,8 @@ import { pipe } from "@mobily/ts-belt";
import { forEach as arrayForEach, map as arrayMap } from "@mobily/ts-belt/Array";
import { EitherAsync, Maybe } from "purify-ts";
import { match, P } from "ts-pattern";
import { type AnySchema, ValiError } from "valibot";
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";

View file

@ -59,9 +59,13 @@ const initialiseObservationFenetre = (): void => {
// Met à jour la valeur du défilement vertical dans la page
defilementY = majDefilementY();
// Vérifie que le Ratio soit le bon
if (ratioActuel < RATIO_MINIMUM_PAGE_PAR_FENETRE) return;
if (ratioActuel < RATIO_MINIMUM_PAGE_PAR_FENETRE) {
return;
}
// Attend la prochaine étape
if (etapePlanifiee) return;
if (etapePlanifiee) {
return;
}
etapePlanifiee = true;
requestAnimationFrame((): void =>

View file

@ -17,12 +17,16 @@ document.addEventListener("DOMContentLoaded", (): void => {
// Créé un nouvel Observer pour la première et dernière entrée.
EffectArray.forEach(firstAndLastEntries, (menuEntry, _index) => {
if (Predicate.isUndefined(menuEntry)) return;
if (Predicate.isUndefined(menuEntry)) {
return;
}
new IntersectionObserver(
EffectArray.forEach((intersectionEntry) => {
// Ne déclenche rien si le scroll n'est pas horizontal
if (intersectionEntry.boundingClientRect.top <= 0) return;
if (intersectionEntry.boundingClientRect.top <= 0) {
return;
}
Match.value([intersectionEntry.isIntersecting]).pipe(
Match.when([true, 0], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-debut")),
@ -32,7 +36,7 @@ document.addEventListener("DOMContentLoaded", (): void => {
Match.orElse(() => {}),
);
}),
{ root: null, threshold: 0.9 },
{ root: undefined, threshold: 0.9 },
).observe(menuEntry);
});
});

View file

@ -102,7 +102,7 @@ const initGestionAnimation = (): void => {
A.at(E.IMAGES_STORYTELLING, 0),
O.tap((img) => {
const options: IntersectionObserverInit = {
root: null,
root: undefined,
rootMargin: "0px",
threshold: 0,
};

View file

@ -92,13 +92,13 @@ const initialisePageBoutique = (): void => {
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
EitherAsync<APIFetchErrors, unknown>(async ({ throwE }) => {
return match(await newPartialResponse(reponse))
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();
}),
.run(),
),
)
// 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCV3ProductsSchema)))
@ -139,12 +139,12 @@ const initialisePageBoutique = (): void => {
</figure>
</article>
`,
tap((article) => fragment.appendChild(article)),
tap((article) => fragment.append(article)),
);
}
// Ajoute les nouveaux Produits dans le DOM
E.GRILLE_PRODUITS.appendChild(fragment);
E.GRILLE_PRODUITS.append(fragment);
E.GRILLE_PRODUITS.setAttribute(ATTRIBUT_PAGE, String(nouveauNumeroPage));
E.BOUTON_PLUS_DE_PRODUITS.textContent = "Show more";

View file

@ -21,7 +21,8 @@ import {
} from "./constantes/dom.ts";
import { NOM_CANAL_BOUTON_PANIER, NOM_CANAL_CONTENU_PANIER } from "./constantes/messages.ts";
import { getDOMElementsWithSelector, recupereElementAvecSelecteur, recupereElementOuLeve } from "./lib/dom.ts";
import { type CleNonTrouveError, reporteErreur } from "./lib/erreurs.ts";
import { reporteErreur } from "./lib/erreurs.ts";
import type { CleNonTrouveError } from "./lib/erreurs.ts";
import { valideMessageMajBoutonPanier, valideMessageMajContenuPanier } from "./lib/messages.ts";
import { arrondisADeuxDecimales, diviseParCent, formateEnEuros, inverseNombre } from "./lib/nombres.ts";
import { propEither } from "./lib/utils.ts";

View file

@ -7,7 +7,8 @@ import { tap as optionTap } from "@mobily/ts-belt/Option";
import { pipe as epipe } from "effect";
import { EitherAsync, Maybe } from "purify-ts";
import { match, P } from "ts-pattern";
import { type AnySchema, ValiError } from "valibot";
import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreCart } from "./lib/types/api/cart";
import type { WCStoreCartAddItemArgs, WCStoreCartAddItemArgsItems } from "./lib/types/api/cart-add-item.ts";
@ -79,8 +80,12 @@ const gereAccordeonDetailsProduit = (): void => {
const idContenu: null | string = bouton.getAttribute(ATTRIBUT_ARIA_CONTROLS);
const sectionCorrespondante: HTMLDivElement | undefined = E.CONTENUS_ACCORDEON[index];
if (!idContenu) throw new Error("Le lien ne dispose pas d'ID !");
if (!sectionCorrespondante) throw new Error("Le lien ne dispose pas de section correspondante !");
if (!idContenu) {
throw new Error("Le lien ne dispose pas d'ID !");
}
if (!sectionCorrespondante) {
throw new Error("Le lien ne dispose pas de section correspondante !");
}
contenus.set(idContenu, [bouton, sectionCorrespondante]);
@ -93,7 +98,9 @@ const gereAccordeonDetailsProduit = (): void => {
pipe(contenus.values(), Array.from<EnsembleLienContenu>, deplieToutesSections);
// Ne fais rien de plus si l'onglet sélectionné était le courant
if (estAncienContenuDeplie) return;
if (estAncienContenuDeplie) {
return;
}
// Ouvre le nouvel onglet sélectionné
bouton.setAttribute(ATTRIBUT_ARIA_EXPANDED, "true");
@ -130,14 +137,14 @@ const getAttributesFromDom = (): ReadonlyArray<WCStoreCartAddItemArgsItems> => {
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>,
);
if (selectElements.length === 0) return [];
if (selectElements.length === 0) {
return [];
}
const attributes = selectElements.map((select: HTMLSelectElement) => {
return {
attribute: select.id,
value: select.value,
} satisfies WCStoreCartAddItemArgsItems;
});
const attributes = selectElements.map((select: HTMLSelectElement) => ({
attribute: select.id,
value: select.value,
}));
return attributes;
};
@ -176,9 +183,9 @@ 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,
// Id: ETATS_PAGE.idProduit,
quantity: 1,
// variation: getAttributeValuesFromDom(),
// Variation: getAttributeValuesFromDom(),
};
// Réalise la Requête et traite sa Réponse

View file

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

View file

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace HaikuAtelier;
use Exception;
use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource;
use Illuminate\Support\Arr;
@ -16,38 +15,36 @@ use Timber\Timber;
use WC_Product;
use WP_Term;
require_once __DIR__ . '/src/inc/TraitementInformations.php';
use function add_action;
use function assert;
use function get_queried_object;
use function is_array;
use function wc_get_products;
$context = Timber::context();
$templates = ['boutique.twig'];
/** @var WP_Term */
/** @var WP_Term La Catégorie affichée. */
$current_term = get_queried_object();
$category_slug = $current_term->slug;
/** @var list<WC_Product> $raw_products Les informations brutes des Produits. */
$raw_products = wc_get_products([
'category' => [$category_slug],
/** @var list<Product> Les Produits de la Catégorie affichée. */
$products = wc_get_products([
'category' => [$current_term->slug],
'limit' => 12,
'order' => 'DESC',
'orderby' => 'date',
'status' => 'publish',
]);
])
|> 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($products, Product::new(...)));
/** @var list<Product> */
$products = Arr::map($raw_products, Product::new(...));
$context['products'] = $products;
$context['category_id'] = $current_term->term_id;
/** @var string */
$products_category_id = array_shift($raw_products)?->get_category_ids()[0] ?? '';
$context['products_category_id'] = $products_category_id;
/**
* Charge les scripts et styles de la page.
*
* @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 {
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-boutique',
path: '/assets/css/pages/page-boutique.css',
@ -60,9 +57,7 @@ function load_page_resources(): void {
id: 'haiku-atelier-2024-scripts-menu-categories',
path: '/assets/js/scripts-menu-categories.js',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));
});
// Rendu
Timber::render(

View file

@ -1,7 +1,7 @@
<div
class="grille-produits"
data-page="1"
{% if products_category_id %}data-id-categorie-produits="{{ products_category_id }}"{% endif %}
{% if category_id %}data-id-categorie-produits="{{ category_id }}"{% endif %}
>
{% if products|length > 0 %}
{% for product in products %}