2026-04-01
This commit is contained in:
parent
ef19ba2b72
commit
5f332f4068
34 changed files with 9392 additions and 391 deletions
|
|
@ -8,15 +8,5 @@
|
|||
"!vtsls",
|
||||
"..."
|
||||
],
|
||||
"languages": {
|
||||
"PHP": {
|
||||
"format_on_save": "on",
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "mago",
|
||||
"arguments": ["format", "--stdin-input"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"languages": {}
|
||||
}
|
||||
|
|
|
|||
3
cfg/oxlint.config.ts
Normal file
3
cfg/oxlint.config.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import config from "@gcch/configuration-oxlint";
|
||||
|
||||
export default config;
|
||||
|
|
@ -1,37 +1,25 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
// Définis un délai d'exécution.
|
||||
timeout: 30000,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "list",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
retries: 1,
|
||||
testDir: "../tests",
|
||||
timeout: 10_000,
|
||||
workers: "100%",
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "https://haikuatelier.gcch.local",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
trace: "retry-with-trace",
|
||||
clientCertificates: [
|
||||
{
|
||||
origin: "https://haikuatelier.gcch.local",
|
||||
certPath: "./containers/data/certs/_wildcard.gcch.local.pem",
|
||||
keyPath: "./containers/data/certs/_wildcard.gcch.local-key.pem",
|
||||
certPath: "../containers/data/certs/_wildcard.gcch.local.pem",
|
||||
keyPath: "../containers/data/certs/_wildcard.gcch.local-key.pem",
|
||||
},
|
||||
],
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "desktop-chromium-1920",
|
||||
|
|
@ -74,10 +62,4 @@ export default defineConfig({
|
|||
// use: { ...devices["Pixel 7 landscape"] },
|
||||
// },
|
||||
],
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@
|
|||
"php-standard-library/phpstan-extension": "^2.1",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^2.1.45",
|
||||
"rector/rector": "^2.3.9",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"szepeviktor/phpstan-wordpress": "2.x-dev",
|
||||
"vincentlanglet/twig-cs-fixer": "^3.14"
|
||||
|
|
|
|||
7381
composer.lock
generated
Normal file
7381
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
4
justfile
4
justfile
|
|
@ -101,7 +101,9 @@ watch-js:
|
|||
[group('qualité')]
|
||||
lint-js:
|
||||
-bun eslint "web/app/themes/haiku-atelier-2024/src/scripts"
|
||||
-bun oxlint "web/app/themes/haiku-atelier-2024/src/scripts"
|
||||
bun --bun oxlint \
|
||||
--config cfg/oxlint.config.ts \
|
||||
--format stylish
|
||||
|
||||
# Vérifie le code Sass avec Stylelint.
|
||||
[group('css')]
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -7,16 +7,15 @@
|
|||
"license": "ISC",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"scripts": { "knip": "knip" },
|
||||
"scripts": {
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/language-service": "^0.60.0",
|
||||
"@mobily/ts-belt": "v4.0.0-rc.5",
|
||||
"@sentry/browser": "^10.47.0",
|
||||
"a11y-dialog": "^8.1.4",
|
||||
"chalk": "^5.6.2",
|
||||
"effect": "^3.21.0",
|
||||
"lit-html": "^3.3.1",
|
||||
"optics-ts": "^2.4.1",
|
||||
"purify-ts": "2.1.2",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"valibot": "1.1.0"
|
||||
|
|
@ -28,7 +27,6 @@
|
|||
"@gcch/configuration-prettier": "git+https://git.gcch.fr/gcch/configuration-prettier#8de937e801",
|
||||
"@playwright/test": "^1.59.0",
|
||||
"@sentry/core": "^10.47.0",
|
||||
"@swc/cli": "0.7.8",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/node": "^25.5.0",
|
||||
"@vitejs/plugin-legacy": "^8.0.1",
|
||||
|
|
@ -49,8 +47,9 @@
|
|||
"lightningcss-cli": "^1.32.0",
|
||||
"oxlint": "^1.58.0",
|
||||
"oxlint-tsgolint": "^0.19.0",
|
||||
"playwright": "^1.59.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-pkg": "^0.21.2",
|
||||
"prettier-plugin-pkg": "^0.22.1",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"stylelint": "^17.6.0",
|
||||
|
|
|
|||
26
rector.php
26
rector.php
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Config\RectorConfig;
|
||||
|
||||
return RectorConfig::configure()
|
||||
->withPaths([__DIR__ . '/web/app/themes/haiku-atelier-2024'])
|
||||
->withSkip([__DIR__ . '/vendor', __DIR__ . '/node_modules'])
|
||||
->withPhpSets(php85: true)
|
||||
->withCodeQualityLevel(10)
|
||||
->withCodingStyleLevel(10)
|
||||
->withDeadCodeLevel(10)
|
||||
->withTypeCoverageDocblockLevel(10)
|
||||
->withTypeCoverageLevel(10)
|
||||
->withImportNames(
|
||||
importDocBlockNames: true,
|
||||
importNames: true,
|
||||
importShortClasses: true,
|
||||
removeUnusedImports: true
|
||||
)
|
||||
->withPreparedSets(
|
||||
carbon: true,
|
||||
instanceOf: true,
|
||||
privatization: true
|
||||
);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/fish
|
||||
|
||||
for image in *.png
|
||||
gm convert -resize 1000 $image ok-$image.png
|
||||
end
|
||||
|
||||
flaca -p *
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
#!/usr/bin/fish
|
||||
|
||||
ssh ade -- fish /srv/haikuatelier.com/scripts/sauvegarde-bdd-production.fish
|
||||
rclone copy --check-first --progress --multi-thread-streams 8 ade:/srv/haikuatelier.com/db /home/gcch/Répertoires/git.gcch.fr/gcch/haiku-atelier-2024/db
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/fish
|
||||
|
||||
set -f fichiers_toml (fd --glob "*.toml")
|
||||
set -f fichiers_angie (fd --glob "*.conf" containers/conf/angie)
|
||||
|
||||
|
|
|
|||
40
scripts/importe-dernier-export-bdd.ts
Normal file
40
scripts/importe-dernier-export-bdd.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { $ } from "bun";
|
||||
import { Array, Option, Order, pipe } from "effect";
|
||||
import { readdir } from "node:fs/promises";
|
||||
|
||||
const launchContainers = async (): Promise<string> => {
|
||||
return await $`podman compose up -d`.text();
|
||||
};
|
||||
|
||||
const getLatestDbExport = async (): Promise<string> => {
|
||||
return pipe(
|
||||
await readdir(`../db`),
|
||||
(paths: ReadonlyArray<string>) => Array.sort(paths, Order.string),
|
||||
(sortedPaths: ReadonlyArray<string>) => Array.last(sortedPaths),
|
||||
(last: Option.Option<string>) =>
|
||||
Option.getOrThrowWith(last, () => new Error("Aucun export de BDD n'est disponible.")),
|
||||
);
|
||||
};
|
||||
|
||||
const importLatestDbInWordpressContainer = async (exportPath: string) => {
|
||||
await $`podman exec -it haikuatelier.fr-wordpress fish -c "cd web && wp --allow-root db import ${exportPath}"`;
|
||||
};
|
||||
|
||||
try {
|
||||
// S'assure que les conteneurs soient lancées.
|
||||
await launchContainers();
|
||||
|
||||
const latestExportPath: string = `../db/${await getLatestDbExport()}`;
|
||||
console.log(`Dernier export : ${latestExportPath}`);
|
||||
|
||||
// Exécute l'opération d'import dans le conteneur WordPress via wp-cli.
|
||||
await importLatestDbInWordpressContainer(latestExportPath);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof $.ShellError) {
|
||||
console.error(`Commande échouée avec code d'erreur: ${error.exitCode}`);
|
||||
console.log(error.stdout.toString());
|
||||
console.log(error.stderr.toString());
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/fish
|
||||
|
||||
cd /srv/haikuatelier.com/web
|
||||
sudo -S wp-cli --allow-root db export
|
||||
sudo -S mv -v /srv/haikuatelier.com/web/*.sql ../db
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/fish
|
||||
|
||||
pyftsubset \
|
||||
lato-variable-italic.ttf \
|
||||
--desubroutinize \
|
||||
|
|
@ -8,7 +8,7 @@ test("can scroll to the end of the grid", async ({ page }): Promise<void> => {
|
|||
await scrollToGridsEnd(page);
|
||||
});
|
||||
|
||||
test("can access all Products' pages", async ({ page, request }): Promise<void> => {
|
||||
test.skip("can access all Products' pages", async ({ page, request }): Promise<void> => {
|
||||
await page.goto("https://haikuatelier.gcch.local/shop/");
|
||||
const links = await getAllProductsLinks(page, request);
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ const scrollToGridsEnd = async (page: Page): Promise<void> => {
|
|||
await expect(showMoreButton, "The 'Show more' button is visible").toBeVisible();
|
||||
|
||||
while (hasMoreProducts) {
|
||||
const newProductsResponse: Promise<Response> = page.waitForResponse(new RegExp(".*wp-json\/wc\/v3\/products.*"));
|
||||
const newProductsResponse: Promise<Response> = page.waitForResponse(new RegExp(".*wp-json/wc/v3/products.*"));
|
||||
await showMoreButton.click();
|
||||
await newProductsResponse;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -21,11 +21,10 @@ use function Crell\fp\pipe;
|
|||
*
|
||||
* @return string TODO
|
||||
*/
|
||||
function genere_balise_img_multiformats($id, bool $lazy = false): string
|
||||
{
|
||||
function genere_balise_img_multiformats(string $id, bool $lazy = false): string {
|
||||
$int_id = (int) $id;
|
||||
|
||||
if (-1 === $id) {
|
||||
if (-1 === $int_id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -43,9 +42,11 @@ function genere_balise_img_multiformats($id, bool $lazy = false): string
|
|||
[$avif, $jxl, $webp],
|
||||
static fn($tableau): array => array_filter(
|
||||
array: $tableau,
|
||||
callback: static fn($chemin_format): bool => false !== $chemin_format,
|
||||
callback: static fn($chemin_format): bool => false !== $chemin_format
|
||||
),
|
||||
static fn($tableau): array => array_map(array: $tableau, callback: static fn($chemin_format): array => [
|
||||
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' =>
|
||||
|
|
@ -53,10 +54,14 @@ function genere_balise_img_multiformats($id, bool $lazy = false): string
|
|||
. '/'
|
||||
. pathinfo($url)['filename']
|
||||
. '.'
|
||||
. pathinfo((string) $chemin_format)['extension'],
|
||||
]),
|
||||
. pathinfo((string) $chemin_format)['extension']
|
||||
]
|
||||
)
|
||||
);
|
||||
usort(
|
||||
array: $formats,
|
||||
callback: static fn($a, $b): int => $a['taille'] <=> $b['taille']
|
||||
);
|
||||
usort(array: $formats, callback: static fn($a, $b): int => $a['taille'] <=> $b['taille']);
|
||||
|
||||
// Construis les balises <source> avec les formats valides
|
||||
$sources = '';
|
||||
|
|
@ -86,8 +91,7 @@ function genere_balise_img_multiformats($id, bool $lazy = false): string
|
|||
/**
|
||||
* TODO.
|
||||
*/
|
||||
function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int
|
||||
{
|
||||
function tri_variations_par_prix_descendant(WC_Product $a, WC_Product $b): int {
|
||||
return $b->get_price() <=> $a->get_price();
|
||||
}
|
||||
|
||||
|
|
@ -97,23 +101,25 @@ 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
|
||||
{
|
||||
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),
|
||||
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,
|
||||
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(),
|
||||
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à.
|
||||
|
|
@ -131,15 +137,15 @@ function recupere_informations_produit_shop(WC_Product $produit): mixed
|
|||
// 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,
|
||||
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,
|
||||
true
|
||||
),
|
||||
// URL du Produit pour les liens vers celui-ci
|
||||
'url' => $produit->get_permalink(),
|
||||
'url' => $produit->get_permalink()
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +154,7 @@ 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
|
||||
{
|
||||
function recupere_informations_produit_page_produit(WC_Product $product): mixed {
|
||||
/** @var list<Attribute> */
|
||||
$attributs = Product::get_attributes_for_product($product);
|
||||
|
||||
|
|
@ -170,19 +175,19 @@ function recupere_informations_produit_page_produit(WC_Product $product): mixed
|
|||
'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'),
|
||||
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'),
|
||||
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,
|
||||
false
|
||||
),
|
||||
'photo_survol' => genere_balise_img_multiformats(
|
||||
get_post_meta($post_id = $product->get_id(), $key = '_photos_colonne_droite|||0|value')[0] ?? -1,
|
||||
true,
|
||||
true
|
||||
),
|
||||
// Slug du Produit
|
||||
'slug' => $product->get_slug(),
|
||||
|
|
@ -191,7 +196,7 @@ function recupere_informations_produit_page_produit(WC_Product $product): mixed
|
|||
// Variations du Produit
|
||||
'variations_ids' => $product->get_children(),
|
||||
// URL du Produit
|
||||
'url' => $product->get_permalink(),
|
||||
'url' => $product->get_permalink()
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -201,8 +206,7 @@ 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
|
||||
{
|
||||
function recupere_produits_meme_collection(string $slug_collection): mixed {
|
||||
// @param int $id_produit
|
||||
return static fn($id_produit) => wc_get_products([
|
||||
'exclude' => [$id_produit],
|
||||
|
|
@ -210,17 +214,16 @@ function recupere_produits_meme_collection(string $slug_collection): mixed
|
|||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'status' => 'publish',
|
||||
'tax_query' => [['taxonomy' => 'collection', 'field' => 'slug', 'terms' => $slug_collection]],
|
||||
'tax_query' => [['taxonomy' => 'collection', 'field' => 'slug', 'terms' => $slug_collection]]
|
||||
]);
|
||||
}
|
||||
|
||||
// Page Panier
|
||||
|
||||
function recupere_et_formate_attributs_produit(mixed $attributs_produit): mixed
|
||||
{
|
||||
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],
|
||||
'cote' => ['nom' => 'Side', 'valeur' => $attributs_produit['pa_side'] ?? false]
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { pipe, Option } from "effect";
|
||||
|
||||
export const getOptionOrThrowWithError = (message: string) => <T>(option: Option.Option<T>): T =>
|
||||
export const getOptionOrThrowWithError =
|
||||
(message: string) =>
|
||||
<T>(option: Option.Option<T>): T =>
|
||||
pipe(
|
||||
option,
|
||||
Option.getOrThrowWith(() => new Error(message)),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
getFirstSelectorFromDocumentOrThrow<HTMLElement>(DOM_MENU_CATEGORIES_PRODUITS);
|
||||
const menuEntries: ReadonlyArray<HTMLAnchorElement> = getAllSelectorFromDocumentOrThrow(
|
||||
DOM_ENTREES_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(() => {}),
|
||||
);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue