2026-04-01

This commit is contained in:
gcch 2026-04-01 17:07:06 +02:00
commit 5f332f4068
34 changed files with 9392 additions and 391 deletions

View file

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

@ -16,28 +16,28 @@ require_once __DIR__ . '/src/inc/TraitementInformations.php';
$context = Timber::context();
$templates = ['produit.twig'];
$product = wc_get_product();
$raw_product = wc_get_product();
// Le Produit DOIT exister.
if ($product === null || is_bool($product)) {
if ($raw_product === null || is_bool($raw_product)) {
throw new Exception("Le Produit n'existe pas.");
}
// Assemble les données d'intérêt pour la page au sein d'une Classe.
$donnees_produit = Product::new($product);
$product = Product::new($raw_product);
/** @var int $prix_maximal Le prix de la Variation la plus chère */
$prix_maximal = collect($donnees_produit->variations)->max('price');
$maximum_price = collect($product->variations)->max('price');
$produits_meme_collection = array_map(
array: recupere_produits_meme_collection($donnees_produit->collection)($donnees_produit->id),
$same_collection_products = array_map(
array: recupere_produits_meme_collection($product->collection)($product->id),
callback: Product::new(...)
);
$context['produit'] = $donnees_produit;
$context['product_json'] = wp_json_encode($donnees_produit);
$context['prix_maximal'] = $prix_maximal;
$context['produits_meme_collection'] = $produits_meme_collection;
$context['product'] = $product;
$context['product_json'] = wp_json_encode($product);
$context['maximum_price'] = $maximum_price;
$context['same_collection_products'] = $same_collection_products;
/**
* Charge les Scripts nécessaires pour la page Produit.
@ -59,9 +59,6 @@ function charge_scripts_page_produit(): void {
add_action('wp_enqueue_scripts', 'charge_scripts_page_produit');
$lal = wp_json_encode($context);
echo "<script>console.debug({$lal});</script>";
// Rendu
Timber::render(
filenames: $templates,

View file

@ -21,74 +21,78 @@ use function Crell\fp\pipe;
*
* @return string TODO
*/
function genere_balise_img_multiformats($id, bool $lazy = false): string
{
$int_id = (int) $id;
function genere_balise_img_multiformats(string $id, bool $lazy = false): string {
$int_id = (int) $id;
if (-1 === $id) {
return '';
}
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) : ['', ''];
$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;
$webp = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.webp') : false;
$avif = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.avif') : false;
$jxl = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.jxl') : false;
$webp = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.webp') : false;
// Génère un tableau avec les différents formats valides
$formats = pipe(
[$avif, $jxl, $webp],
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']);
// Génère un tableau avec les différents formats valides
$formats = pipe(
[$avif, $jxl, $webp],
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";
}
// 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';
$loading = $lazy ? 'lazy' : 'eager';
return <<<EOD
{$sources}
return <<<EOD
{$sources}
<img
alt="{$alt}"
decoding="async"
height="{$dimensions[0]}"
loading="{$loading}"
onload="this.style.opacity=1"
src="{$url}"
width="{$dimensions[1]}"
/>
EOD;
<img
alt="{$alt}"
decoding="async"
height="{$dimensions[0]}"
loading="{$loading}"
onload="this.style.opacity=1"
src="{$url}"
width="{$dimensions[1]}"
/>
EOD;
}
/**
* TODO.
*/
function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int
{
return $b->get_price() <=> $a->get_price();
function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int {
return $b->get_price() <=> $a->get_price();
}
/**
@ -97,50 +101,52 @@ function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int
*
* @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(),
);
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 = '';
}
// 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(),
];
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
@ -148,51 +154,50 @@ function recupere_informations_produit_shop(WC_Product $produit): mixed
/**
* 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);
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(),
];
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()
];
}
/**
@ -201,26 +206,24 @@ function recupere_informations_produit_page_produit(WC_Product $product): mixed
*
* Pour faciliter l'usage avec `array_map`, utilise une fonction avec curryfication.
*/
function recupere_produits_meme_collection(string $slug_collection): mixed
{
// @param int $id_produit
return static fn($id_produit) => wc_get_products([
'exclude' => [$id_produit],
'limit' => 4,
'order' => 'DESC',
'orderby' => 'date',
'status' => 'publish',
'tax_query' => [['taxonomy' => 'collection', 'field' => 'slug', 'terms' => $slug_collection]],
]);
function recupere_produits_meme_collection(string $slug_collection): mixed {
// @param int $id_produit
return static fn($id_produit) => 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],
];
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

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

@ -7,8 +7,9 @@ import { getOptionOrThrowWithError } from "./utils";
export type ParentElement = Document | Element;
export const getFirstSelectorFromParent =
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
Option.fromNullishOr(parent.querySelector<E>(selector));
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
Option.fromNullable(parent.querySelector<E>(selector));
export const getFirstSelectorFromDocument = <E extends Element = Element>(
selector: string,
@ -21,12 +22,13 @@ export const getFirstSelectorFromDocumentOrThrow = <E extends Element = Element>
);
export const getAllSelectorFromParent =
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
pipe(
parent.querySelectorAll<E>(selector),
// Convertis NodeListOf en Array.
Array.from<E>,
(xs: Array<E>) => Option.liftPredicate(EffectArray.isReadonlyArrayNonEmpty)(xs),
(xs: Array<E>) => Option.liftPredicate(EffectArray.isNonEmptyReadonlyArray)(xs),
);
export const getAllSelectorFromDocument = <E extends Element = Element>(

View file

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

View file

@ -4,13 +4,7 @@ import { match } from "ts-pattern";
import type { HttpCodeErrors, SimplifiedResponse } from "./types/reseau";
import { ENTETE_WC_NONCE } from "../constantes/api.ts";
import {
BadRequestError,
ForbiddenError,
NotFoundError,
ServerError,
UnauthorizedError,
} from "./erreurs.ts";
import { BadRequestError, ForbiddenError, NotFoundError, ServerError, UnauthorizedError } from "./erreurs.ts";
// Types
@ -59,9 +53,7 @@ export const getBackend = (args: ArgumentsGetBackendWC): Promise<Response> =>
signal: AbortSignal.timeout(5000),
});
export const getBackendAvecParametresUrl = (
args: ArgumentsGetBackendWC,
): Promise<Response> =>
export const getBackendAvecParametresUrl = (args: ArgumentsGetBackendWC): Promise<Response> =>
fetch(`${args.route}?${args.searchParams}`, {
credentials: "same-origin",
headers: {
@ -76,9 +68,7 @@ export const getBackendAvecParametresUrl = (
signal: AbortSignal.timeout(5000),
});
export const deleteBackend = (
args: ArgumentsDeleteBackendWC,
): Promise<Response> =>
export const deleteBackend = (args: ArgumentsDeleteBackendWC): Promise<Response> =>
fetch(args.route, {
credentials: "same-origin",
headers: {
@ -111,11 +101,7 @@ export const postBackend = (args: ArgumentsPostBackendWC): Promise<Response> =>
export const prefilledPostBackend =
(nonce: string, authString?: string) =>
(
route: string,
body: BodyInit,
needsAuthString: boolean,
): Promise<Response> =>
(route: string, body: BodyInit, needsAuthString: boolean): Promise<Response> =>
fetch(route, {
body: body,
credentials: "same-origin",
@ -123,32 +109,25 @@ export const prefilledPostBackend =
Accept: "application/json",
"Content-Type": "application/json",
[ENTETE_WC_NONCE]: nonce,
...(authString &&
needsAuthString && { Authorization: `Basic ${authString}` }),
...(authString && needsAuthString && { Authorization: `Basic ${authString}` }),
},
method: "POST",
mode: "same-origin",
signal: AbortSignal.timeout(5000),
});
export const safeFetch = (
f: Promise<Response>,
): EitherAsync<DOMException | TypeError, Response> =>
export const safeFetch = (f: Promise<Response>): EitherAsync<DOMException | TypeError, Response> =>
EitherAsync<DOMException | TypeError, Response>(async () => await f);
// Réponses Simplifiées
export const newPartialResponse = async (
reponse: Response,
): Promise<SimplifiedResponse> => {
export const newPartialResponse = async (reponse: Response): Promise<SimplifiedResponse> => {
return {
body: await reponse.json(),
status: reponse.status,
};
};
export const traiteErreursBackendWooCommerce = (
rs: SimplifiedResponse,
): HttpCodeErrors => {
export const traiteErreursBackendWooCommerce = (rs: SimplifiedResponse): HttpCodeErrors => {
return match(rs)
.with({ status: 400 }, () => new BadRequestError())
.with({ status: 401 }, () => new UnauthorizedError())

View file

@ -2,28 +2,18 @@
import { Array as EffectArray, Match, Predicate } from "effect";
import {
DOM_ENTREES_MENU_CATEGORIES_PRODUITS,
DOM_MENU_CATEGORIES_PRODUITS,
} from "./constantes/dom.ts";
import {
getAllSelectorFromDocumentOrThrow,
getFirstSelectorFromDocumentOrThrow,
} from "../scripts-effect/lib/dom.ts";
import { DOM_ENTREES_MENU_CATEGORIES_PRODUITS, DOM_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
import { getAllSelectorFromDocumentOrThrow, getFirstSelectorFromDocumentOrThrow } from "../scripts-effect/lib/dom.ts";
// Initialise les attributs HTML pour l'affichage initiale des flèches de défilement du menu de catégories de Produits.
document.addEventListener("DOMContentLoaded", (): void => {
const productsCategoriesMenu: HTMLElement =
getFirstSelectorFromDocumentOrThrow<HTMLElement>(
DOM_MENU_CATEGORIES_PRODUITS,
);
const menuEntries: ReadonlyArray<HTMLAnchorElement> =
getAllSelectorFromDocumentOrThrow(DOM_ENTREES_MENU_CATEGORIES_PRODUITS);
getFirstSelectorFromDocumentOrThrow<HTMLElement>(DOM_MENU_CATEGORIES_PRODUITS);
const menuEntries: ReadonlyArray<HTMLAnchorElement> = getAllSelectorFromDocumentOrThrow(
DOM_ENTREES_MENU_CATEGORIES_PRODUITS,
);
const firstAndLastEntries: Array<HTMLAnchorElement | undefined> = [
menuEntries.at(0),
menuEntries.at(-1),
];
const firstAndLastEntries: Array<HTMLAnchorElement | undefined> = [menuEntries.at(0), menuEntries.at(-1)];
// Créé un nouvel Observer pour la première et dernière entrée.
EffectArray.forEach(firstAndLastEntries, (menuEntry, _index) => {
@ -35,28 +25,10 @@ document.addEventListener("DOMContentLoaded", (): void => {
if (intersectionEntry.boundingClientRect.top <= 0) return;
Match.value([intersectionEntry.isIntersecting]).pipe(
Match.when([true, 0], () =>
productsCategoriesMenu.removeAttribute(
"data-entrees-presentes-debut",
),
),
Match.when([true, 1], () =>
productsCategoriesMenu.removeAttribute(
"data-entrees-presentes-fin",
),
),
Match.when([false, 0], () =>
productsCategoriesMenu.setAttribute(
"data-entrees-presentes-debut",
"",
),
),
Match.when([false, 1], () =>
productsCategoriesMenu.setAttribute(
"data-entrees-presentes-fin",
"",
),
),
Match.when([true, 0], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-debut")),
Match.when([true, 1], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-fin")),
Match.when([false, 0], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-debut", "")),
Match.when([false, 1], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-fin", "")),
Match.orElse(() => {}),
);
}),

View file

@ -173,8 +173,7 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
// Construis les arguments de la requête au backend
const argsRequete: WCStoreCartAddItemArgs = {
id: E.DOM_VARIATION
.map((selecteur: HTMLSelectElement): number => Number(selecteur.value))
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,

View file

@ -6,61 +6,62 @@ declare(strict_types=1);
* Le modèle de la Page d'Archive d'une Catégorie de Produits.
*/
use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource;
use Timber\Timber;
require_once __DIR__ . '/src/inc/TraitementInformations.php';
// Contexte et modèles
$contexte = Timber::context();
$modeles = ['boutique.twig'];
$context = Timber::context();
$templates = ['boutique.twig'];
/** @var list<WC_Product> $informations_produits Les informations brutes des Produits. */
$informations_produits = wc_get_products([
'category' => [get_queried_object()?->slug],
/** @var WP_Term */
$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],
'limit' => 12,
'order' => 'DESC',
'orderby' => 'date',
'status' => 'publish'
]);
/** @var InformationsProduitShop $produits Les informations strictement nécessaires pour la grille des Produits. */
$produits = array_map(
callback: recupere_informations_produit_shop(...),
array: $informations_produits
$products = array_map(
callback: Product::new(...),
array: $raw_products
);
$contexte['produits'] = $produits;
$id_categorie_produits = array_shift($informations_produits)?->get_category_ids()[0] ?? '';
$contexte['id_categorie_produits'] = $id_categorie_produits;
$context['products'] = $products;
$products_category_id = array_shift($raw_products)?->get_category_ids()[0] ?? '';
$context['products_category_id'] = $products_category_id;
/**
* Charge les Scripts nécessaires pour la page d'Archive.
* Charge les ressources nécessaires pour la page d'Archive.
*/
function charge_scripts_page_archive_produits(): void {
wp_enqueue_style(
function load_page_resources(): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-boutique',
src: get_template_directory_uri() . '/assets/css/pages/page-boutique.css',
deps: [],
ver: filemtime(get_template_directory() . '/assets/css/pages/page-boutique.css'),
media: 'all'
path: '/assets/css/pages/page-boutique.css'
);
wp_enqueue_script_module(
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-boutique',
src: get_template_directory_uri() . '/assets/js/scripts-page-boutique.js',
deps: [],
version: filemtime(get_template_directory() . '/assets/js/scripts-page-boutique.js')
path: '/assets/js/scripts-page-boutique.js'
);
wp_enqueue_script_module(
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-menu-categories',
src: get_template_directory_uri() . '/assets/js/scripts-menu-categories.js',
deps: [],
version: filemtime(get_template_directory() . '/assets/js/scripts-menu-categories.js')
path: '/assets/js/scripts-menu-categories.js'
);
}
add_action('wp_enqueue_scripts', 'charge_scripts_page_archive_produits');
add_action('wp_enqueue_scripts', 'load_page_resources');
$lal = wp_json_encode($context);
echo "<script>console.debug({$lal});</script>";
// Rendu
Timber::render(
filenames: $modeles,
data: $contexte
filenames: $templates,
data: $context
);

View file

@ -24,7 +24,7 @@ const _etats = {
<div class="actions">
<button
{{ produits|length == 12 ? '' : 'hidden' }}
{{ products|length == 12 ? '' : 'hidden' }}
class="bouton-case-pleine bouton-blanc-sur-noir"
id="bouton-plus-de-produits"
type="button"

View file

@ -1,24 +1,24 @@
<div class="grille-produits-similaires">
{% for produit in produits_meme_collection %}
{% for product in same_collection_products %}
{# TODO: Trouver une meilleure arborescence et des noms de classe #}
<article class="produit">
<figure role="figure">
<a href="{{ produit.url }}">
<a href="{{ product.url }}">
<picture class="produit__illustration produit__illustration__principale">
{{ produit.default_photo }}
{{ product.default_photo }}
</picture>
<picture class="produit__illustration produit__illustration__survol">
{{ produit.hover_photo }}
{{ product.hover_photo }}
</picture>
</a>
<figcaption class="produit__textuel">
<h3 class="produit__textuel__titre">
<a href="{{ produit.url }}">{{ produit.name }}</a>
<a href="{{ product.url }}">{{ product.name }}</a>
</h3>
<p class="produit__textuel__prix">
{{ produit.price }}
{{ product.price }}
</p>
</figcaption>
</figure>

View file

@ -9,11 +9,11 @@
id="variation-choice"
name="variation-choice"
>
<h3 class="selecteur-produit__nom">{{ produit.name }}</h3>
<h3 class="selecteur-produit__nom">{{ product.name }}</h3>
<div class="selecteur-produit__attribut-variation">
{% if produit.attributes %}
{% for attribut in produit.attributes %}
{% if product.attributes %}
{% for attribut in product.attributes %}
<div class="test">
{{ include('parts/pages/produit/selecteur-attributs-produit.twig') }}
</div>
@ -56,7 +56,7 @@
#}
</div>
<p class="selecteur-produit__prix">{{ prix_maximal ?? produit.price }}€</p>
<p class="selecteur-produit__prix">{{ maximum_price ?? product.price }}€</p>
</form>
</aside>
@ -79,7 +79,7 @@
class="section-textuelle__contenu"
id="section-details-produit"
>
{{ produit.details }}
{{ product.details }}
</div>
</section>
@ -122,7 +122,7 @@
<div class="details-produit__actions">
{# Désactive le bouton d'ajout au panier en cas d'absence de stock. #}
{% if produit.stock > 0 %}
{% if product.stock > 0 %}
<button
class="bouton-case-pleine"
disabled

View file

@ -3,7 +3,7 @@
aria-label="Photo of the Product alone"
class="colonne colonne-gauche"
>
{% for photo in produit.left_column_photos %}
{% for photo in product.left_column_photos %}
<figure
data-index="0"
role="figure"
@ -19,7 +19,7 @@
aria-label="Photos of the Product worn"
class="colonne colonne-droite"
>
{% for photo in produit.right_column_photos %}
{% for photo in product.right_column_photos %}
<figure
data-index="{{ loop.index }}"
role="figure"

View file

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

View file

@ -13,7 +13,7 @@
/** @type {Etats} */
const _etats = {
idProduit: {{ produit.id }},
idProduit: {{ product.id }},
nonce: "{{ nonce_wc }}",
};
</script>
@ -38,7 +38,7 @@
{{ include('parts/pages/produit/informations-produit.twig') }}
{# Produits de la même Collection (Produits similaires) #}
{% if produit.collection != '' %}
{% if product.collection != '' and same_collection_products|length > 0 %}
{{ include('parts/pages/produit/produits-similaires.twig') }}
{% endif %}
{% endblock contenu %}