2025-01-08

This commit is contained in:
gcch 2024-12-24 15:18:08 +01:00
commit caf87cf1da
38 changed files with 1541 additions and 1401 deletions

View file

@ -29,16 +29,22 @@
"no-async-await": "off",
"no-console": "off",
"no-magic-numbers": "warn",
"no-map-spread": "off",
"no-misused-promises": "off",
"no-optional-chaining": "off",
"no-rest-spread-properties": "off",
"no-ternary": "off",
"no-undefined": "off",
"no-unused-expressions": "off",
"no-void": "off",
"prefer-await-to-then": "off",
"promise/prefer-await-to-callbacks": "off",
"sort-imports": "off",
"typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }],
"typescript/consistent-indexed-object-style": ["error", "record"],
"typescript/consistent-type-imports": "error",
"prefer-await-to-then": "off",
"no-void": "off",
"no-optional-chaining": "off",
"no-unused-expressions": "off",
"no-misused-promises": "off",
"typescript/explicit-function-return-type": "warn",
"unicorn/prefer-dom-node-dataset": "off",
"yoda": ["error", "never"]
}
}

79
STUFF.md Normal file
View file

@ -0,0 +1,79 @@
I'll see you soup
- Thèmes
- LS_COLORS (Vivid ?)
- bat
- eza (?)
- fdfind
- fish
- gitui
- helix
- wezterm
- yazi
- zed
- zellij
Google API
AIzaSyDGe62r-bDxvNuDCP6HIfWIJAMvelFxU1s
402628219773-hl8niqniiiklf15f9biou8g06pbm9sac.apps.googleusercontent.com
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
- Code promo ?
- La commande peut être associée au panier.
- Vu que l'on passe par l'API REST, le panier est dissocié de la commande.
- Il est possible de fixer le hash du panier dans la commande avec la fonction set_cart_hash de WC_Order.
- Cela permet par la suite d'utiliser la fonction cancel_order WC_Order à l'annulation de cette dernière lors du retour au Panier depuis Stripe.
- Ce ne sera possible qu'en utilisant un endpoint personnalisé réalisant ces opérations plutôt que l'API REST.
- Dans l'idéal,
- Ajouter un bouton "Reset cart" quelque part pour tout réinitialiser (et appeler cancel_order si implémenté)
---
- BadRequestError
- reponse.status === 400
- reponse.body = {
code: string,
message: string (différenciation sur le message ?),
data: {
status: number (400),
}
}
---
Stripe
pk_live_51D0BbTIKBol0AhpghF9b6lJ4ZjPXWaNRzBgxtcUTdbV8OC2OpHxSbkMoEEgCHEPSs6E6NISfdMv92t9OnKqKh0sH00N6tgi6HW
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
---
- Chargement de la page
- Récupération des informations à la génération de la page
- Panier
- Code promo
- Mode de livraison
- Sous-totaux
- Total
- Adresses
- Récupération des informations dans le LocalStorage
- Code promo
- Mode de livraison
- Adresses
- À l'injection de données du LocalStorage
- Mettre à jour les sous-totaux
- À l'appui sur le bouton de calcul de la livraison et au succès de la requête
- Mettre à jour les méthodes de livraison
- Mettre à jour les sous-totaux et le total
- Sauvegarder les nouvelles données dans le LocalStorage
- Événements à créer
- MiseAJourCodePromo
- Se déclenche quand le champ du Code promo est modifié
- MiseAJourProduits
- Se déclenche quand une des lignes du Panier est modifiée (addition/soustraction/suppression)
- MiseAJourMethodeLivraison
- Se déclence quand le choix de la Méthode de livraison est modifié
- MiseAJourAdresses
- Se déclenche quand un des champs du formulaire des adresses est modifié

153
composer.lock generated
View file

@ -646,16 +646,16 @@
},
{
"name": "illuminate/collections",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
"reference": "21868f9ac221a42d4346dc56495d11ab7e0d339a"
"reference": "9100b083eeb85d38d78fc1de28f7326596ab2eda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/collections/zipball/21868f9ac221a42d4346dc56495d11ab7e0d339a",
"reference": "21868f9ac221a42d4346dc56495d11ab7e0d339a",
"url": "https://api.github.com/repos/illuminate/collections/zipball/9100b083eeb85d38d78fc1de28f7326596ab2eda",
"reference": "9100b083eeb85d38d78fc1de28f7326596ab2eda",
"shasum": ""
},
"require": {
@ -698,11 +698,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-12-13T13:58:10+00:00"
"time": "2024-12-18T14:14:45+00:00"
},
{
"name": "illuminate/conditionable",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@ -748,7 +748,7 @@
},
{
"name": "illuminate/contracts",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@ -796,7 +796,7 @@
},
{
"name": "illuminate/macroable",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@ -842,16 +842,16 @@
},
{
"name": "illuminate/support",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "fba1ff58e30fa280248ce3db9b18d6341c6ac339"
"reference": "388c916b143a104e732cbaf7e6b19cd7a4e21a1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/fba1ff58e30fa280248ce3db9b18d6341c6ac339",
"reference": "fba1ff58e30fa280248ce3db9b18d6341c6ac339",
"url": "https://api.github.com/repos/illuminate/support/zipball/388c916b143a104e732cbaf7e6b19cd7a4e21a1e",
"reference": "388c916b143a104e732cbaf7e6b19cd7a4e21a1e",
"shasum": ""
},
"require": {
@ -875,7 +875,7 @@
},
"suggest": {
"illuminate/filesystem": "Required to use the Composer class (^11.0).",
"laravel/serializable-closure": "Required to use the once function (^1.3).",
"laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).",
"league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).",
"league/uri": "Required to use the Uri class (^7.5.1).",
"ramsey/uuid": "Required to use Str::uuid() (^4.7).",
@ -915,7 +915,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-12-17T20:20:09+00:00"
"time": "2024-12-20T14:43:22+00:00"
},
{
"name": "laravel/helpers",
@ -1223,16 +1223,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.8.3",
"version": "3.8.4",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "f01cfa96468f4c38325f507ab81a4f1d2cd93cfe"
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "129700ed449b1f02d70272d2ac802357c8c30c58"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f01cfa96468f4c38325f507ab81a4f1d2cd93cfe",
"reference": "f01cfa96468f4c38325f507ab81a4f1d2cd93cfe",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/129700ed449b1f02d70272d2ac802357c8c30c58",
"reference": "129700ed449b1f02d70272d2ac802357c8c30c58",
"shasum": ""
},
"require": {
@ -1325,7 +1325,7 @@
"type": "tidelift"
}
],
"time": "2024-12-21T18:03:19+00:00"
"time": "2024-12-27T09:25:35+00:00"
},
{
"name": "oscarotero/env",
@ -2381,12 +2381,12 @@
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
@ -2901,16 +2901,16 @@
},
{
"name": "symfony/translation",
"version": "v7.2.0",
"version": "v7.2.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5"
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
"shasum": ""
},
"require": {
@ -2976,7 +2976,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.2.0"
"source": "https://github.com/symfony/translation/tree/v7.2.2"
},
"funding": [
{
@ -2992,7 +2992,7 @@
"type": "tidelift"
}
],
"time": "2024-11-12T20:47:56+00:00"
"time": "2024-12-07T08:18:10+00:00"
},
{
"name": "symfony/translation-contracts",
@ -3013,12 +3013,12 @@
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
@ -3148,21 +3148,21 @@
},
{
"name": "timber/timber",
"version": "v2.3.0",
"version": "v2.3.1",
"source": {
"type": "git",
"url": "https://github.com/timber/timber.git",
"reference": "55acea4414eac6ea9d0a11a102af37cf13f219b2"
"reference": "3f6e73feadf5d547dff4992f645805da7fbc4d3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/timber/timber/zipball/55acea4414eac6ea9d0a11a102af37cf13f219b2",
"reference": "55acea4414eac6ea9d0a11a102af37cf13f219b2",
"url": "https://api.github.com/repos/timber/timber/zipball/3f6e73feadf5d547dff4992f645805da7fbc4d3a",
"reference": "3f6e73feadf5d547dff4992f645805da7fbc4d3a",
"shasum": ""
},
"require": {
"php": "^8.1",
"twig/twig": "^3.5"
"twig/twig": "^3.17"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.28",
@ -3177,7 +3177,7 @@
"squizlabs/php_codesniffer": "^3.0",
"symplify/easy-coding-standard": "^12.2",
"szepeviktor/phpstan-wordpress": "^1.1",
"twig/cache-extra": "^3.3",
"twig/cache-extra": "^3.17",
"wpackagist-plugin/advanced-custom-fields": "^6.0",
"wpackagist-plugin/co-authors-plus": "^3.6",
"yoast/wp-test-utils": "^1.2"
@ -3246,20 +3246,20 @@
"type": "open_collective"
}
],
"time": "2024-11-08T09:38:16+00:00"
"time": "2024-12-23T13:04:50+00:00"
},
{
"name": "twig/twig",
"version": "v3.17.1",
"version": "v3.18.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "677ef8da6497a03048192aeeb5aa3018e379ac71"
"reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/677ef8da6497a03048192aeeb5aa3018e379ac71",
"reference": "677ef8da6497a03048192aeeb5aa3018e379ac71",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"shasum": ""
},
"require": {
@ -3314,7 +3314,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.17.1"
"source": "https://github.com/twigphp/Twig/tree/v3.18.0"
},
"funding": [
{
@ -3326,7 +3326,7 @@
"type": "tidelift"
}
],
"time": "2024-12-12T09:58:10+00:00"
"time": "2024-12-29T10:51:50+00:00"
},
{
"name": "vlucas/phpdotenv",
@ -3596,15 +3596,15 @@
},
{
"name": "wpackagist-plugin/woocommerce",
"version": "9.5.1",
"version": "9.5.2",
"source": {
"type": "svn",
"url": "https://plugins.svn.wordpress.org/woocommerce/",
"reference": "tags/9.5.1"
"reference": "tags/9.5.2"
},
"dist": {
"type": "zip",
"url": "https://downloads.wordpress.org/plugin/woocommerce.9.5.1.zip"
"url": "https://downloads.wordpress.org/plugin/woocommerce.9.5.2.zip"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
@ -3614,15 +3614,15 @@
},
{
"name": "wpackagist-plugin/wp-mail-logging",
"version": "1.13.1",
"version": "1.14.0",
"source": {
"type": "svn",
"url": "https://plugins.svn.wordpress.org/wp-mail-logging/",
"reference": "tags/1.13.1"
"reference": "tags/1.14.0"
},
"dist": {
"type": "zip",
"url": "https://downloads.wordpress.org/plugin/wp-mail-logging.1.13.1.zip"
"url": "https://downloads.wordpress.org/plugin/wp-mail-logging.1.14.0.zip"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
@ -3650,15 +3650,15 @@
},
{
"name": "wpackagist-plugin/wp-openapi",
"version": "1.0.17",
"version": "1.0.18",
"source": {
"type": "svn",
"url": "https://plugins.svn.wordpress.org/wp-openapi/",
"reference": "tags/1.0.17"
"reference": "tags/1.0.18"
},
"dist": {
"type": "zip",
"url": "https://downloads.wordpress.org/plugin/wp-openapi.1.0.17.zip"
"url": "https://downloads.wordpress.org/plugin/wp-openapi.1.0.18.zip"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
@ -3784,16 +3784,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.0.4",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524"
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d276fc3bf1430ec315f2f109bbde2769821524",
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"shasum": ""
},
"require": {
@ -3838,7 +3838,7 @@
"type": "github"
}
],
"time": "2024-12-17T17:14:01+00:00"
"time": "2025-01-05T16:43:48+00:00"
},
{
"name": "roave/security-advisories",
@ -3846,12 +3846,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "abbccc97f36a9c78f033525c019d310433f22b57"
"reference": "19bf84017a308ac32893551b899bac74d2aba856"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/abbccc97f36a9c78f033525c019d310433f22b57",
"reference": "abbccc97f36a9c78f033525c019d310433f22b57",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/19bf84017a308ac32893551b899bac74d2aba856",
"reference": "19bf84017a308ac32893551b899bac74d2aba856",
"shasum": ""
},
"conflict": {
@ -3973,7 +3973,7 @@
"datatables/datatables": "<1.10.10",
"david-garcia/phpwhois": "<=4.3.1",
"dbrisinajumi/d2files": "<1",
"dcat/laravel-admin": "<=2.1.3",
"dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta",
"derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3",
"derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4",
"desperado/xml-bundle": "<=0.1.7",
@ -4095,6 +4095,7 @@
"grumpydictator/firefly-iii": "<6.1.17",
"gugoan/economizzer": "<=0.9.0.0-beta1",
"guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5",
"guzzlehttp/oauth-subscriber": "<0.8.1",
"guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5",
"haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2",
"harvesthq/chosen": "<1.8.7",
@ -4140,7 +4141,7 @@
"james-heinrich/phpthumb": "<1.7.12",
"jasig/phpcas": "<1.3.3",
"jcbrand/converse.js": "<3.3.3",
"joelbutcher/socialstream": "<6.2",
"joelbutcher/socialstream": "<5.6|>=6,<6.2",
"johnbillion/wp-crontrol": "<1.16.2",
"joomla/application": "<1.0.13",
"joomla/archive": "<1.1.12|>=2,<2.0.1",
@ -4255,10 +4256,11 @@
"neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9",
"neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2",
"neos/swiftmailer": "<5.4.5",
"netcarver/textile": "<=4.1.2",
"netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15",
"nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
"nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
"nilsteampassnet/teampass": "<3.0.10",
"nilsteampassnet/teampass": "<3.1.3.1-dev",
"nonfiction/nterchange": "<4.1.1",
"notrinos/notrinos-erp": "<=0.7",
"noumo/easyii": "<=0.9",
@ -4319,10 +4321,10 @@
"phpmailer/phpmailer": "<6.5",
"phpmussel/phpmussel": ">=1,<1.6",
"phpmyadmin/phpmyadmin": "<5.2.1",
"phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5",
"phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1",
"phpoffice/common": "<0.2.9",
"phpoffice/phpexcel": "<1.8.1",
"phpoffice/phpspreadsheet": "<1.29.4|>=2,<2.1.3|>=2.2,<2.3.2|>=3.3,<3.4",
"phpoffice/phpspreadsheet": "<=1.29.6|>=2,<=2.1.5|>=2.2,<=2.3.4|>=3,<3.7",
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
"phpservermon/phpservermon": "<3.6",
"phpsysinfo/phpsysinfo": "<3.4.3",
@ -4401,7 +4403,7 @@
"shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev",
"shopxo/shopxo": "<=6.1",
"showdoc/showdoc": "<2.10.4",
"shuchkin/simplexlsx": ">=1.0.12,<1.1.12",
"shuchkin/simplexlsx": ">=1.0.12,<1.1.13",
"silverstripe-australia/advancedreports": ">=1,<=2",
"silverstripe/admin": "<1.13.19|>=2,<2.1.8",
"silverstripe/assets": ">=1,<1.11.1",
@ -4448,6 +4450,7 @@
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
"ssddanbrown/bookstack": "<24.05.1",
"starcitizentools/citizen-skin": ">=2.6.3,<2.31",
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2",
"statamic/cms": "<=5.16",
"stormpath/sdk": "<9.9.99",
"studio-42/elfinder": "<=2.1.64",
@ -4512,18 +4515,20 @@
"t3s/content-consent": "<1.0.3|>=2,<2.0.2",
"tastyigniter/tastyigniter": "<3.3",
"tcg/voyager": "<=1.4",
"tecnickcom/tcpdf": "<=6.7.5",
"tecnickcom/tc-lib-pdf-font": "<2.6.4",
"tecnickcom/tcpdf": "<6.8",
"terminal42/contao-tablelookupwizard": "<3.3.5",
"thelia/backoffice-default-template": ">=2.1,<2.1.2",
"thelia/thelia": ">=2.1,<2.1.3",
"theonedemon/phpwhois": "<=4.2.5",
"thinkcmf/thinkcmf": "<6.0.8",
"thorsten/phpmyfaq": "<4",
"thorsten/phpmyfaq": "<=4.0.1",
"tikiwiki/tiki-manager": "<=17.1",
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
"tinymce/tinymce": "<7.2",
"tinymighty/wiki-seo": "<1.2.2",
"titon/framework": "<9.9.99",
"tltneon/lgsl": "<7",
"tobiasbg/tablepress": "<=2.0.0.0-RC1",
"topthink/framework": "<6.0.17|>=6.1,<=8.0.4",
"topthink/think": "<=6.1.1",
@ -4601,7 +4606,7 @@
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
"yeswiki/yeswiki": "<=4.4.4",
"yetiforce/yetiforce-crm": "<=6.4",
"yetiforce/yetiforce-crm": "<6.5",
"yidashi/yii2cmf": "<=2",
"yii2mod/yii2-cms": "<1.9.2",
"yiisoft/yii": "<1.1.29",
@ -4691,7 +4696,7 @@
"type": "tidelift"
}
],
"time": "2024-12-20T16:05:39+00:00"
"time": "2025-01-07T18:06:22+00:00"
},
{
"name": "squizlabs/php_codesniffer",

View file

@ -79,7 +79,7 @@
"https://plugins.dprint.dev/typescript-0.93.3.wasm",
"https://plugins.dprint.dev/json-0.19.4.wasm",
"https://plugins.dprint.dev/markdown-0.17.8.wasm",
"https://plugins.dprint.dev/toml-0.6.3.wasm",
"https://plugins.dprint.dev/toml-0.6.4.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.11.1.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.18.0.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm",

View file

@ -8,57 +8,57 @@
"packageManager": "pnpm@9.14.4",
"main": "index.js",
"keywords": [],
"scripts": {
"knip": "knip",
"test": "echo \"Error: no test specified\" && exit 1"
},
"scripts": { "knip": "knip", "test": "echo \"Error: no test specified\" && exit 1" },
"dependencies": {
"@mobily/ts-belt": "4.0.0-rc.5",
"@sentry/browser": "8.47.0",
"@sentry/browser": "8.48.0",
"@swan-io/boxed": "^3.2.0",
"a11y-dialog": "^8.1.1",
"chalk": "^5.4.1",
"lit-html": "^3.2.1",
"loglevel": "^1.9.2",
"loglevel-plugin-prefix": "^0.8.4",
"optics-ts": "^2.4.1",
"purify-ts": "^2.1.0",
"ts-pattern": "^5.6.0",
"valibot": "1.0.0-beta.9"
"valibot": "1.0.0-beta.11"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@eslint/js": "^9.17.0",
"@prettier/plugin-php": "^0.22.2",
"@prettier/plugin-xml": "^3.4.1",
"@sentry/core": "^8.47.0",
"@sentry/core": "^8.48.0",
"@swc/cli": "0.5.2",
"@types/eslint__js": "^8.42.3",
"@types/node": "^22.10.2",
"@types/node": "^22.10.5",
"@vitejs/plugin-legacy": "^6.0.0",
"better-typescript-lib": "^2.10.0",
"browserslist": "^4.24.3",
"browserslist": "^4.24.4",
"eslint": "^9.17.0",
"eslint-plugin-oxlint": "^0.15.2",
"eslint-plugin-perfectionist": "^4.4.0",
"eslint-plugin-oxlint": "^0.15.5",
"eslint-plugin-perfectionist": "^4.6.0",
"fdir": "^6.4.2",
"globals": "^15.14.0",
"knip": "^5.41.1",
"oxlint": "^0.15.3",
"oxlint": "^0.15.5",
"picomatch": "^4.0.2",
"prettier": "^3.4.2",
"prettier-plugin-pkg": "^0.18.1",
"prettier-plugin-sh": "^0.14.0",
"sass-embedded": "^1.83.0",
"sass-embedded": "^1.83.1",
"stylelint": "^16.12.0",
"stylelint-config-clean-order": "^6.1.0",
"stylelint-config-clean-order": "^7.0.0",
"stylelint-config-sass-guidelines": "^12.1.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
"stylelint-plugin-logical-css": "^1.2.1",
"typescript": "5.8.0-dev.20241122",
"typescript-eslint": "^8.18.1",
"vite": "^6.0.5",
"typescript-eslint": "^8.19.1",
"vite": "^6.0.7",
"vite-plugin-manifest-sri": "^0.2.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-valibot-env": "^0.9.2",
"vite-plugin-valibot-env": "^0.9.3",
"vite-tsconfig-paths": "^5.1.4",
"wp-types": "^4.67.0"
},
@ -72,15 +72,8 @@
"ios >0 and last 3 years"
],
"knip": {
"entry": [
"web/app/themes/haiku-atelier-2024/src/scripts/*.ts"
],
"project": [
"web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"
]
"entry": ["web/app/themes/haiku-atelier-2024/src/scripts/*.ts"],
"project": ["web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"]
},
"trustedDependencies": [
"@biomejs/biome",
"@parcel/watcher"
]
"trustedDependencies": ["@biomejs/biome", "@parcel/watcher"]
}

1127
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
export const ADRESSES_MAJ = "adressesMaj";
export const CODE_PROMO_MAJ = "codePromoMaj";
export const METHODES_LIVRAISON_MAJ = "methodesLivraisonMaj";
export const SHIPPING_RATES_UPDATED = "shippingRatesUpdated";
export const PRODUITS_MAJ = "produitsMaj";
export const TOTAUX_MAJ = "totauxMaj";
export const TOTALS_UPDATED = "totalsUpdated";

View file

@ -73,4 +73,4 @@ const client = new BrowserClient({
/* Initialise la configuration */
getCurrentScope().setClient(client);
client.init();
// client.init();

View file

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

View file

@ -1,19 +1,25 @@
import { A, G, pipe } from "@mobily/ts-belt";
import { Either, identity, Left, Maybe, Right } from "purify-ts";
import { Either, identity, Left, Right } from "purify-ts";
import type { ElementParent } from "./types/dom.d.ts";
import type { ParentElement } from "./types/dom.d.ts";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_DESACTIVE } from "../constantes/dom.ts";
import { logger } from "../logging.ts";
import { lanceAnimationCycleLoading } from "./animations.ts";
import {
BadRequestError,
creeSyntaxError,
ERREUR_SELECTEUR_INEXISTANT,
ERREUR_SYNTAXE_INVALIDE,
ErreurEntreeInexistante,
type NonExistingKeyError,
ForbiddenError,
NotFoundError,
reporteEtLeveErreur,
ServerError,
UnauthorizedError,
} from "./erreurs";
export const recupereElementAvecSelecteur =
(parent: ElementParent) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, E> =>
(parent: ParentElement) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, E> =>
Either
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => parent.querySelector<E>(selecteur))
@ -24,8 +30,8 @@ export const recupereElementAvecSelecteur =
G.isNotNullable(e) ? Right(e) : Left(creeSyntaxError(ERREUR_SELECTEUR_INEXISTANT(selecteur)))
);
export const recupereElementsAvecSelecteur =
(parent: ElementParent) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, Array<E>> =>
export const getDOMElementsWithSelector =
(parent: ParentElement) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, Array<E>> =>
Either
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => pipe(parent.querySelectorAll<E>(selecteur), Array.from<E>))
@ -62,29 +68,86 @@ export const html = (strings: TemplateStringsArray, ...args: Array<string>) =>
template => template.content,
);
/**
* Récupère une entrée dans le Stockage de Session (`storageSession`) sous forme d'`Either`.
*
* @param cle La clé de l'entrée.
* @returns Un `Either` avec une `NonExistingKeyError` si la clé est absente (`Left`), la
* valeur de l'entrée sinon (`Right`).
*/
export const eitherSessionStorageGet = (cle: string): Either<NonExistingKeyError, string> =>
Maybe
.fromNullable(sessionStorage.getItem(cle))
.toEither(ErreurEntreeInexistante(`Clé ${cle} absente dans le stockage de session.`));
/**
* Convertis une chaîne JSON en un objet JavaScript sous forme d'`Either`.
* @param chaine La chaîne à convertir.
* @returns Un `Either` avec une `SyntaxError` si la chaîne est invalide, un objet JS sinon
* (`Right`).
*/
export const eitherJsonParse = (chaine: string): Either<SyntaxError, JSONValue> =>
export const safeJsonParse = (chaine: string): Either<SyntaxError, JSONValue> =>
Either.encase(() => JSON.parse(chaine));
/** TODO */
export const accorderCibleASelecteur = <E extends HTMLElement = HTMLElement>(
/**
* Vérifie qu'un sélecteur s'applique à l'élément DOM d'une cible d'événement (un `EventTarget`) donnée.
* La fonction agit comme un garde de type.
*
* @returns Un booléen
*/
export const targetMatchesSelector = <E extends HTMLElement = HTMLElement>(
cible: EventTarget | null,
selecteur: string,
): cible is E => cible !== null && (cible as HTMLElement).matches(selecteur);
export const recupereElementsDocumentEither: <E extends Element = Element>(
selecteur: string,
) => Either<SyntaxError, Array<E>> = getDOMElementsWithSelector(document);
export const recupereElementDocumentEither: <E extends Element = Element>(
selecteur: string,
) => Either<SyntaxError, E> = recupereElementAvecSelecteur(document);
/**
* Fonction utilitaire pour récupérer un Élément selon un sélecteur au sein du Document.
*
* @param selecteur Le sélecteur de l'Élément recherché.
* @throws Une SyntaxError si l'Élément n'est pas trouvé.
* @returns Un Élément.
*/
export const mustGetEleInDocument = <E extends Element = Element>(selecteur: string): E =>
pipe(
recupereElementDocumentEither<E>(selecteur),
recupereElementOuLeve,
);
export const mustGetEleInParent = (parent: ParentElement) => <E extends HTMLElement>(selector: string) =>
pipe(recupereElementAvecSelecteur(parent)<E>(selector), recupereElementOuLeve);
/**
* Fonction utilitaire pour récupérer des Éléments selon un sélecteur au sein du Document.
*
* @param selecteur Le sélecteur des Éléments recherchés.
* @throws Une SyntaxError si aucun Élément n'est trouvé.
* @returns Un tableau d'Éléments.
*/
export const mustGetElesInDocument = <E extends Element = Element>(selecteur: string): Array<E> =>
pipe(
recupereElementsDocumentEither<E>(selecteur),
recupereElementsOuLeve,
);
export const setButtonLoadingState = (button: HTMLButtonElement, isLoading: boolean): void => {
logger.debug("majEtatChargementBouton", button, isLoading);
if (isLoading) {
// Désactive le Bouton pour empêcher des requêtes concurrentes
button.setAttribute(ATTRIBUT_DESACTIVE, "");
button.setAttribute(ATTRIBUT_CHARGEMENT, "");
// Lance un cycle d'animation sur le texte de chargement
lanceAnimationCycleLoading(button, 500);
} else {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
button.removeAttribute(ATTRIBUT_CHARGEMENT);
button.removeAttribute(ATTRIBUT_DESACTIVE);
}
};
export const estErreurHttp = (erreur: unknown) =>
erreur instanceof BadRequestError
|| erreur instanceof ForbiddenError
|| erreur instanceof NotFoundError
|| erreur instanceof ServerError
|| erreur instanceof UnauthorizedError;
export const estErreurFetch = (erreur: unknown) =>
erreur instanceof DOMException
|| erreur instanceof TypeError
|| erreur instanceof Error;

View file

@ -1,19 +1,26 @@
import type { WCStoreCartTotals, WCStoreShippingRateShippingRate } from "../types/api/cart";
import { ADRESSES_MAJ, CODE_PROMO_MAJ, METHODES_LIVRAISON_MAJ, TOTAUX_MAJ } from "../../constantes/evenements";
import { ADRESSES_MAJ, CODE_PROMO_MAJ, SHIPPING_RATES_UPDATED, TOTALS_UPDATED } from "../../constantes/evenements";
export const ADRESSES_MAJ_EVENT = new CustomEvent(ADRESSES_MAJ, {});
export const CODE_PROMO_MAJ_EVENT = new CustomEvent(CODE_PROMO_MAJ, {});
export const MethodesLivraisonMajEvent = (methodes: ReadonlyArray<WCStoreShippingRateShippingRate>) =>
new CustomEvent(METHODES_LIVRAISON_MAJ, { detail: { methodes } });
// Interfaces
export interface MethodesLivraisonMajEvent extends Event {
detail: { methodes: ReadonlyArray<WCStoreShippingRateShippingRate> };
export interface UpdatedShippingRatesEvent extends Event {
detail: { refresh_methods: boolean; shipping_rates: ReadonlyArray<WCStoreShippingRateShippingRate> };
}
export interface UpdatedTotalsEvent extends Event {
detail: { totals: WCStoreCartTotals };
}
export const TotauxMajEvent = (totaux: WCStoreCartTotals) => new CustomEvent(TOTAUX_MAJ, { detail: { totaux } });
// Méthodes
export interface TotauxMajEvent extends Event {
detail: { totaux: WCStoreCartTotals };
}
export const createUpdatedShippingRatesEvent = (
shipping_rates: ReadonlyArray<WCStoreShippingRateShippingRate>,
refresh_methods: boolean,
): UpdatedShippingRatesEvent =>
new CustomEvent(SHIPPING_RATES_UPDATED, { detail: { refresh_methods, shipping_rates } });
export const createUpdatedTotalsEvent = (totals: WCStoreCartTotals): UpdatedTotalsEvent =>
new CustomEvent(TOTALS_UPDATED, { detail: { totals } });

View file

@ -2,9 +2,9 @@ import type { GenericSchema, InferOutput, ValiError } from "valibot";
import { Either, Maybe } from "purify-ts";
import { eitherJsonParse } from "./dom.ts";
import { safeJsonParse } from "./dom.ts";
import { ErreurEntreeInexistante, type NonExistingKeyError } from "./erreurs.ts";
import { eitherValiParseCurried } from "./validation.ts";
import { safeSchemaParse, safeSchemaParseCurried } from "./validation.ts";
export type GetLocalStorage<S extends GenericSchema> = Either<ErreursGetLocalStorage<S>, InferOutput<S>>;
type ErreursGetLocalStorage<S extends GenericSchema> =
@ -24,13 +24,18 @@ export const eitherGetLocalStorage = (cle: string): Either<NonExistingKeyError,
.fromNullable(localStorage.getItem(cle))
.toEither(ErreurEntreeInexistante(`Clé ${cle} absente dans le stockage de session.`));
export const setLocalStorage = <V>(cle: string, valeur: V): Either<DOMException, V> =>
export const eitherSetLocalStorage = <V>(cle: string, valeur: V): Either<DOMException, V> =>
Either.encase<DOMException, V>(() => {
localStorage.setItem(cle, JSON.stringify(valeur));
return valeur;
});
export const getAndParseLocalStorage = <S extends GenericSchema>(cle: string, schema: S): GetLocalStorage<S> =>
eitherGetLocalStorage(cle)
.chain(eitherJsonParse)
.chain(eitherValiParseCurried(schema));
export const getLocalStorageByKey = <S extends GenericSchema>(key: string, schema: S): GetLocalStorage<S> =>
eitherGetLocalStorage(key)
.chain(safeJsonParse)
.chain(safeSchemaParseCurried(schema));
export const setLocalStorageByKey =
<S extends GenericSchema>(key: string, schema: S) =>
(value: unknown): Either<DOMException | ValiError<S>, InferOutput<S>> =>
safeSchemaParse(value, schema).chain(v => eitherSetLocalStorage(key, v));

View file

@ -8,13 +8,13 @@ import type {
MessageMajContenuPanier,
MessageMajContenuPanierDonnees,
} from "./types/messages";
import type { ReponseSimplifiee } from "./types/reseau";
import type { SimplifiedResponse } from "./types/reseau";
import { NOM_CANAL_BOUTON_PANIER, NOM_CANAL_CONTENU_PANIER, TYPES_MESSAGES } from "../constantes/messages.ts";
import { reporteErreur } from "./erreurs.ts";
import { WCErrorSchema } from "./schemas/api/erreurs.ts";
import { MessageMajBoutonPanierSchema, MessageMajContenuPanierSchema } from "./schemas/messages.ts";
import { eitherValiParse } from "./validation.ts";
import { safeSchemaParse } from "./validation.ts";
const canalPostMessage = (canal: BroadcastChannel, message: unknown): BroadcastChannel => {
canal.postMessage(message);
@ -77,7 +77,7 @@ export const valideMessageMajContenuPanier = (
.ifLeft(erreur => reporteErreur(erreur));
// Correspondances
export const reponseEstCodeErreurWC = (reponse: ReponseSimplifiee, codeErreurWC: string): boolean =>
eitherValiParse(reponse, WCErrorSchema)
export const reponseEstCodeErreurWC = (reponse: SimplifiedResponse, codeErreurWC: string): boolean =>
safeSchemaParse(reponse, WCErrorSchema)
.map(v => v.body.code === codeErreurWC)
.orDefault(false);

View file

@ -3,7 +3,7 @@ import { EitherAsync } from "purify-ts";
import { match, P } from "ts-pattern";
import { type GenericSchema, parse } from "valibot";
import type { HttpCodeErrors, ReponseSimplifiee } from "./types/reseau";
import type { HttpCodeErrors, SimplifiedResponse } from "./types/reseau";
import { ENTETE_WC_NONCE } from "../constantes/api.ts";
import {
@ -126,7 +126,27 @@ export const postBackend = (args: ArgumentsPostBackendWC): Promise<Response> =>
},
);
export const eitherAsyncFetch = (f: Promise<Response>): EitherAsync<DOMException | TypeError, Response> =>
export const prefilledPostBackend =
(nonce: string, authString?: string) =>
(route: string, body: BodyInit, needsAuthString: boolean): Promise<Response> =>
fetch(
route,
{
body: body,
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
[ENTETE_WC_NONCE]: nonce,
...(authString && needsAuthString && { Authorization: `Basic ${authString}` }),
},
method: "POST",
mode: "same-origin",
signal: AbortSignal.timeout(5000),
},
);
export const safeFetch = (f: Promise<Response>): EitherAsync<DOMException | TypeError, Response> =>
EitherAsync<DOMException | TypeError, Response>(async () => await f);
// TODO: Ne traite pas du tout les Erreurs
@ -146,14 +166,14 @@ export const traiteReponseBackendWCSelonCodesHTTP = <R, S extends GenericSchema<
.otherwise(e => pipe(e, ErreurInconnue, leveErreur<UnknownError>));
// Réponses Simplifiées
export const creeReponseSimplifiee = async (reponse: Response): Promise<ReponseSimplifiee> => {
export const newPartialResponse = async (reponse: Response): Promise<SimplifiedResponse> => {
return {
body: await reponse.json(),
status: reponse.status,
};
};
export const traiteErreursBackendWooCommerce = (rs: ReponseSimplifiee): HttpCodeErrors => {
export const traiteErreursBackendWooCommerce = (rs: SimplifiedResponse): HttpCodeErrors => {
return match(rs)
.with({ status: 400 }, () => new BadRequestError())
.with({ status: 401 }, () => new UnauthorizedError())

View file

@ -0,0 +1,11 @@
import { Maybe } from "purify-ts";
/**
* Retourne sous forme d'un Maybe le premier élément d'un tableau.
* @param xs Le tableau dont on souhaite le premier élément.
* @returns Un Just avec le premier élément du tableau, un None sinon.
*/
export const first = <T>(xs: Array<T>): Maybe<T> => Maybe.fromNullable(xs.at(0));
export const find = <T>(predicateFn: (_1: T) => boolean) => (xs: Array<T>): Maybe<T> =>
Maybe.fromNullable(xs.find(predicateFn));

View file

@ -1,9 +1,13 @@
import type { InferOutput } from "valibot";
import type { WCStoreCartItemSchema, WCStoreCartSchema, WCStoreCartTotalsSchema } from "../../schemas/api/cart.ts";
import type { WCStoreShippingRateShippingRateSchema } from "../../schemas/api/couts-livraison.ts";
import type {
WCStoreShippingRateSchema,
WCStoreShippingRateShippingRateSchema,
} from "../../schemas/api/couts-livraison.ts";
export type WCStoreCart = InferOutput<typeof WCStoreCartSchema>;
export type WCStoreCartItem = InferOutput<typeof WCStoreCartItemSchema>;
export type WCStoreCartTotals = InferOutput<typeof WCStoreCartTotalsSchema>;
export type WCStoreShippingRate = InferOutput<typeof WCStoreShippingRateSchema>;
export type WCStoreShippingRateShippingRate = InferOutput<typeof WCStoreShippingRateShippingRateSchema>;

View file

@ -1,2 +1,2 @@
/** Type union des parents possibles pour un `querySelector`. */
export type ElementParent = Document | Element;
export type ParentElement = Document | Element;

View file

@ -1,4 +1,4 @@
export type EtatsPageGenerique = {
export type GenericPageState = {
/** Une chaîne pour l'authentification de requêtes vers la nouvelle API (v3) du backend WooCommerce. */
authString: string;
/** Un nonce pour l'authentification de requêtes API vers le backend WooCommerce. */

View file

@ -10,7 +10,7 @@ export type HttpCodeErrors =
| ServerError
| UnauthorizedError;
export interface ReponseSimplifiee {
export interface SimplifiedResponse {
body: unknown;
status: number;
}

View file

@ -1,30 +1,7 @@
import { D, pipe } from "@mobily/ts-belt";
import { D } from "@mobily/ts-belt";
import { type Either, Maybe } from "purify-ts";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_DESACTIVE } from "../constantes/dom.ts";
import { lanceAnimationCycleLoading } from "./animations.ts";
import {
recupereElementAvecSelecteur,
recupereElementOuLeve,
recupereElementsAvecSelecteur,
recupereElementsOuLeve,
} from "./dom.ts";
import {
BadRequestError,
CleNonTrouveError,
ForbiddenError,
NotFoundError,
ServerError,
UnauthorizedError,
} from "./erreurs.ts";
export const recupereElementsDocumentEither: <E extends Element = Element>(
selecteur: string,
) => Either<SyntaxError, Array<E>> = recupereElementsAvecSelecteur(document);
export const recupereElementDocumentEither: <E extends Element = Element>(
selecteur: string,
) => Either<SyntaxError, E> = recupereElementAvecSelecteur(document);
import { CleNonTrouveError } from "./erreurs";
/**
* TODO
@ -35,55 +12,3 @@ export const propEither = <T, K extends keyof T>(cle: K) => (donnees: T): Either
.toEither(
new CleNonTrouveError(`La clé « ${String(cle)} » n'a pas été trouvé dans l'objet.`),
);
/**
* Fonction utilitaire pour récupérer un Élément selon un sélecteur au sein du Document.
*
* @param selecteur Le sélecteur de l'Élément recherché.
* @throws Une SyntaxError si l'Élément n'est pas trouvé.
* @returns Un Élément.
*/
export const recupereEleOuLeve = <E extends Element = Element>(selecteur: string): E =>
pipe(
recupereElementDocumentEither<E>(selecteur),
recupereElementOuLeve,
);
/**
* Fonction utilitaire pour récupérer des Éléments selon un sélecteur au sein du Document.
*
* @param selecteur Le sélecteur des Éléments recherchés.
* @throws Une SyntaxError si aucun Élément n'est trouvé.
* @returns Un tableau d'Éléments.
*/
export const recupereElesOuLeve = <E extends Element = Element>(selecteur: string): Array<E> =>
pipe(
recupereElementsDocumentEither<E>(selecteur),
recupereElementsOuLeve,
);
export const majEtatChargementBouton = (bouton: HTMLButtonElement, activation: boolean): void => {
if (activation) {
// Désactive le Bouton pour empêcher des requêtes concurrentes
bouton.setAttribute(ATTRIBUT_DESACTIVE, "");
bouton.setAttribute(ATTRIBUT_CHARGEMENT, "");
// Lance un cycle d'animation sur le texte de chargement
lanceAnimationCycleLoading(bouton, 500);
} else {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
bouton.removeAttribute(ATTRIBUT_CHARGEMENT);
bouton.removeAttribute(ATTRIBUT_DESACTIVE);
}
};
export const estErreurHttp = (erreur: unknown) =>
erreur instanceof BadRequestError
|| erreur instanceof ForbiddenError
|| erreur instanceof NotFoundError
|| erreur instanceof ServerError
|| erreur instanceof UnauthorizedError;
export const estErreurFetch = (erreur: unknown) =>
erreur instanceof DOMException
|| erreur instanceof TypeError
|| erreur instanceof Error;

View file

@ -5,11 +5,11 @@
import { Either } from "purify-ts";
import { type GenericSchema, type InferOutput, parse, type ValiError } from "valibot";
export const eitherValiParse = <Schema extends GenericSchema>(
export const safeSchemaParse = <Schema extends GenericSchema>(
valeur: unknown,
schema: Schema,
): Either<ValiError<Schema>, InferOutput<Schema>> => Either.encase(() => parse(schema, valeur));
export const eitherValiParseCurried =
export const safeSchemaParseCurried =
<S extends GenericSchema>(schema: S) => (valeur: unknown): Either<ValiError<S>, InferOutput<S>> =>
Either.encase(() => parse(schema, valeur));

View file

@ -0,0 +1,21 @@
import chalk from "chalk";
import log, { type Logger } from "loglevel";
import prefix from "loglevel-plugin-prefix";
const colors = {
DEBUG: chalk.cyan,
ERROR: chalk.red,
INFO: chalk.blue,
TRACE: chalk.magenta,
WARN: chalk.yellow,
};
export const logger: Logger = log.noConflict() as Logger;
logger.enableAll(true);
logger.setDefaultLevel("DEBUG");
prefix.reg(logger);
prefix.apply(logger, {
format(level, _, timestamp) {
return `${chalk.gray(`[${timestamp}]`)} ${colors[level.toUpperCase()](level)}`;
},
});

View file

@ -1,30 +1,20 @@
import { pipe } from "@mobily/ts-belt";
import { find as arrayFind, map as arrayMap } from "@mobily/ts-belt/Array";
import { find as arrayFind } from "@mobily/ts-belt/Array";
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 type { WCStoreCart, WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreBillingAddress, WCStoreShippingAddress } from "../lib/types/api/adresses";
import type { WCStoreCart, WCStoreShippingRate, WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreCartUpdateCustomerArgs } from "../lib/types/api/cart-update-customer";
import type { WCStoreShippingRateShippingRates } from "../lib/types/api/couts-livraison";
import type { WCV3Order, WCV3OrdersArgs } from "../lib/types/api/v3/orders";
import type { EtatsPageGenerique } from "../lib/types/pages";
import type { GenericPageState } from "../lib/types/pages";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau";
import { ROUTE_API_MAJ_CLIENT, ROUTE_API_NOUVELLE_COMMANDES } from "../constantes/api";
import {
ATTRIBUT_CHARGEMENT,
ATTRIBUT_LIVRAISON_VALIDEE,
SELECTEUR_BOUTON_ACTIONS_FORMULAIRE,
SELECTEUR_BOUTON_SEPARATION_ADRESSES,
SELECTEUR_CONTENEUR_METHODES_LIVRAISON,
SELECTEUR_ENTREES_PANIER,
SELECTEUR_FORMULAIRE_PANIER,
SELECTEUR_INSTRUCTIONS_CLIENT,
SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES,
} from "../constantes/dom";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_LIVRAISON_VALIDEE } from "../constantes/dom";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages";
import {
ERREUR_ADRESSE_MAUVAIS_CODE_POSTAL,
@ -32,42 +22,36 @@ import {
ERREUR_GENERIQUE_RESEAU,
ERREUR_GENERIQUE_SOUMISSION_ADRESSES,
} from "../constantes/messages-utilisateur";
import { leveErreur, type NonExistingKeyError, reporteErreur, reporteEtJournaliseErreur } from "../lib/erreurs";
import { estErreurFetch, estErreurHttp, setButtonLoadingState } from "../lib/dom";
import { reporteEtJournaliseErreur } from "../lib/erreurs";
import { ErreurAdresseInvalide } from "../lib/erreurs/adresses";
import { ADRESSES_MAJ_EVENT, MethodesLivraisonMajEvent, TotauxMajEvent } from "../lib/evenements/panier";
import { getAndParseLocalStorage } from "../lib/local-storage";
import {
ADRESSES_MAJ_EVENT,
createUpdatedShippingRatesEvent,
createUpdatedTotalsEvent,
} from "../lib/evenements/panier";
import { emetUniqueMessageBroadcastChannel } from "../lib/messages";
import { diviseParCent } from "../lib/nombres";
import { creeReponseSimplifiee, eitherAsyncFetch, postBackend, traiteErreursBackendWooCommerce } from "../lib/reseau";
import { newPartialResponse, prefilledPostBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau";
import { find, first } from "../lib/safe-arrays";
import { WCStoreCartSchema } from "../lib/schemas/api/cart";
import { WCStoreCartUpdateCustomerArgsSchema } from "../lib/schemas/api/cart-update-customer";
import { WCStoreShippingRateShippingRatesSchema } from "../lib/schemas/api/couts-livraison";
import { estWCAddressError } from "../lib/schemas/api/erreurs";
import { WCV3OrdersArgsSchema, WCV3OrderSchema } from "../lib/schemas/api/v3/orders";
import {
estErreurFetch,
estErreurHttp,
majEtatChargementBouton,
recupereElementsDocumentEither,
recupereEleOuLeve,
} from "../lib/utils";
import { eitherValiParse } from "../lib/validation";
import { genereHtmlMethodesLivraison } from "./scripts-page-panier-methodes-livraison";
import { safeSchemaParse } from "../lib/validation";
import { logger } from "../logging";
import { E } from "./scripts-page-panier-elements";
import { getShippingRatesLS } from "./scripts-page-panier-local-storage";
interface 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
const ETATS_PAGE: EtatsPageGenerique = _etats;
const E = {
BOUTON_ACTIONS_FORMULAIRE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_ACTIONS_FORMULAIRE),
BOUTON_SEPARATION_ADRESSES: recupereEleOuLeve<HTMLInputElement>(SELECTEUR_BOUTON_SEPARATION_ADRESSES),
CONTENEUR_METHODES_LIVRAISON: recupereEleOuLeve<HTMLFieldSetElement>(SELECTEUR_CONTENEUR_METHODES_LIVRAISON),
ENTREES_PANIER_EITHER: recupereElementsDocumentEither<HTMLElement>(SELECTEUR_ENTREES_PANIER),
FORMULAIRE_PANIER: recupereEleOuLeve<HTMLFormElement>(SELECTEUR_FORMULAIRE_PANIER),
INSTRUCTIONS_CLIENT: recupereEleOuLeve<HTMLTextAreaElement>(SELECTEUR_INSTRUCTIONS_CLIENT),
MESSAGE_ADRESSES: recupereEleOuLeve<HTMLParagraphElement>(SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES),
};
const ETATS_PAGE: GenericPageState = _etats;
const postBackend = prefilledPostBackend(ETATS_PAGE.nonce, ETATS_PAGE.authString);
/**
* Initialise les Émetteurs d'Événements sur divers parties du Panier.
*
@ -75,126 +59,138 @@ const E = {
*
* @returns void
*/
export const initialiseEmetteursEvenementsFormulairePanier = (): void => {
export const initCartFormEventEmiters = (): void => {
logger.debug("ADRESSES", "initCartFormEventEmiters");
E.FORMULAIRE_PANIER.addEventListener("change", (): void => {
logger.info("ADRESSES", "Changement du Formulaire Panier");
Maybe
.fromFalsy(E.FORMULAIRE_PANIER.checkValidity())
.ifJust((): boolean => window.dispatchEvent(ADRESSES_MAJ_EVENT));
});
};
export const initialiseBoutonCalculLivraison = (): void => {
// Déclenche la requête pour la soumission des adresses
export const getAddressesFromForm = (formFields: Record<string, string>, areAddressesMerged: boolean): Addresses => {
logger.debug("ADDRESSES", "getAddressesFromForm");
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 initShippingCalculationButton = (): void => {
logger.debug("ADRESSES", "initShippingCalculationButton");
// Déclenche au clic sur le Bouton de soumission du Formulaire la requête pour le calcul des frais de livraison
E.BOUTON_ACTIONS_FORMULAIRE.addEventListener("click", (event: Event): void => {
logger.info("ADRESSES", "Demande de calcul des frais de livraison de la commande");
Maybe
// Ne fais rien si le Formulaire n'est pas valide
.fromFalsy(E.FORMULAIRE_PANIER.checkValidity())
// Ne fais rien si la livraison a déjà été validée
.chainNullable((): boolean | null =>
E.BOUTON_ACTIONS_FORMULAIRE.hasAttribute(ATTRIBUT_LIVRAISON_VALIDEE) ? null : true
.chainNullable((): boolean | undefined =>
E.BOUTON_ACTIONS_FORMULAIRE.hasAttribute(ATTRIBUT_LIVRAISON_VALIDEE) ? undefined : true
)
.ifJust((): void => {
event.preventDefault();
/** Les données du Formulaire sans caractères vides. */
const donneesFormulaire: Record<string, string> = pipe(
/** Les données du Formulaire transformées pour la requête vers le Backend. */
const formArgs: WCStoreCartUpdateCustomerArgs = pipe(
Object.fromEntries(new FormData(E.FORMULAIRE_PANIER)) as Record<string, string>,
dictMap(stringTrim),
fields => dictMap(fields, stringTrim),
fields => getAddressesFromForm(fields, E.BOUTON_SEPARATION_ADRESSES.checked),
);
/** Les données du Formulaire transformées en arguments pour la requête vers le Backend. */
const argumentsFormulaire: WCStoreCartUpdateCustomerArgs = {
billing_address: {
address_1: donneesFormulaire["facturation-adresse"] ?? donneesFormulaire["livraison-adresse"] ?? "",
address_2: "",
city: donneesFormulaire["facturation-ville"] ?? donneesFormulaire["livraison-ville"] ?? "",
company: "",
country: E.BOUTON_SEPARATION_ADRESSES.checked
? donneesFormulaire["facturation-pays"] ?? ""
: donneesFormulaire["livraison-pays"] ?? "",
email: donneesFormulaire["facturation-email"] ?? donneesFormulaire["livraison-email"] ?? "",
first_name: donneesFormulaire["facturation-prenom"] ?? donneesFormulaire["livraison-prenom"] ?? "",
last_name: donneesFormulaire["facturation-nom"] ?? donneesFormulaire["livraison-nom"] ?? "",
phone: donneesFormulaire["facturation-telephone"] ?? donneesFormulaire["livraison-telephone"] ?? "",
postcode: donneesFormulaire["facturation-code-postal"] ?? donneesFormulaire["livraison-code-postal"] ?? "",
state: donneesFormulaire["facturation-region-etat"] ?? donneesFormulaire["livraison-region-etat"] ?? "",
},
shipping_address: {
address_1: donneesFormulaire["livraison-adresse"] ?? "",
address_2: "",
city: donneesFormulaire["livraison-ville"] ?? "",
company: "",
country: donneesFormulaire["livraison-pays"] ?? "",
first_name: donneesFormulaire["livraison-prenom"] ?? "",
last_name: donneesFormulaire["livraison-nom"] ?? "",
phone: donneesFormulaire["livraison-telephone"] ?? "",
postcode: donneesFormulaire["livraison-code-postal"] ?? "",
state: donneesFormulaire["livraison-region-etat"] ?? "",
},
};
logger.debug("ADRESSES", "initShippingCalculationButton", "formArgs", formArgs);
// Réalise la requête et traite sa réponse
void EitherAsync
.liftEither(eitherValiParse(argumentsFormulaire, WCStoreCartUpdateCustomerArgsSchema))
.ifRight((): void => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
majEtatChargementBouton(E.BOUTON_ACTIONS_FORMULAIRE, true);
})
.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) =>
eitherAsyncFetch(
postBackend({ corps: JSON.stringify(args), nonce: ETATS_PAGE.nonce, route: ROUTE_API_MAJ_CLIENT }),
)
safeFetch(postBackend(ROUTE_API_MAJ_CLIENT, JSON.stringify(args), false))
)
.chain((rs: Response) =>
EitherAsync<ErreurAdresseInvalide | HttpCodeErrors, unknown>(async ({ throwE }): Promise<unknown> =>
match(await creeReponseSimplifiee(rs))
.chain((rs: Response) => {
logger.debug("ADRESSES", "initShippingCalculationButton", "rs", rs);
return EitherAsync<ErreurAdresseInvalide | HttpCodeErrors, unknown>(async ({ throwE }): Promise<unknown> =>
match(await newPartialResponse(rs))
.with({ status: 200 }, (rs): unknown => rs.body)
.with(
{ body: P.when(body => estWCAddressError(body)), status: 400 },
(rs): never => throwE(new ErreurAdresseInvalide(rs.body.data.params)),
)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs)))
)
)
.chain((body: unknown) => EitherAsync.liftEither(eitherValiParse(body, WCStoreCartSchema)))
);
})
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCStoreCartSchema)))
.ifRight((cart: WCStoreCart): void => {
const localStorageSelectedMethod = getAndParseLocalStorage(
"shipping_rates",
WCStoreShippingRateShippingRatesSchema,
)
.toMaybe()
.chainNullable(arrayFind(m => m.selected));
logger.debug("ADRESSES", "initShippingCalculationButton", "cart", cart);
const updatedMethods = Maybe
.fromNullable(cart.shipping_rates.at(0))
.chainNullable(xs => xs.shipping_rates)
.map(arrayMap(m => {
// Sélectionne la Méthode précédemment choisie
localStorageSelectedMethod.ifJust(mls => {
m.selected = m.method_id === mls.method_id;
});
m.price = diviseParCent(m.price);
return m;
}))
.ifJust(xs => {
// Met à jour les Méthodes de livraison
window.dispatchEvent(MethodesLivraisonMajEvent(xs));
/** La méthode de livraison sélectionnée dans le LocalStorage */
const oldSelectedRateLS = getShippingRatesLS()
.chain(find(sr => sr.selected))
.ifJust(sr => logger.debug("ADRESSES", "initShippingCalculationButton", "oldSelectedRateLS", sr));
// Met à jour les Totaux
const selectedMethod = xs.find(m => m.selected)!;
const newTotals = {
...cart.totals,
total_discount: diviseParCent(cart.totals.total_discount),
total_items: diviseParCent(cart.totals.total_items),
total_price: diviseParCent(cart.totals.total_items) - diviseParCent(cart.totals.total_discount)
+ selectedMethod.price,
total_shipping: selectedMethod.price,
};
window.dispatchEvent(TotauxMajEvent(newTotals));
})
/* Les méthodes de livraison mises à jour avec le nouveau choix de l'Utilisateur. */
const updatedRates = first(cart.shipping_rates)
.chainNullable((sr: WCStoreShippingRate) => sr.shipping_rates)
.map((srs: Array<WCStoreShippingRateShippingRate>) =>
srs.map((sr: WCStoreShippingRateShippingRate, index: number) => {
// Sélectionne la nouvelle méthode demandée OU la première si le LocalStorage n'a pas été défini
oldSelectedRateLS.caseOf({
Just: sm => {
sr.selected = sr.method_id === sm.method_id;
},
Nothing: () => {
sr.selected = index === 0;
},
});
// Formate le prix correctement
sr.price = diviseParCent(sr.price);
return sr;
})
)
.orDefault([]);
logger.debug("ADRESSES", "initShippingCalculationButton", "updatedRates", updatedRates);
// Met à jour les Méthodes de livraison dans le LocalStorage et le DOM
window.dispatchEvent(createUpdatedShippingRatesEvent(updatedRates, true));
// Met à jour les Totaux
const newShippingPrice = updatedRates.find(m => m.selected)?.price ?? 0;
const newTotals = {
...cart.totals,
total_discount: diviseParCent(cart.totals.total_discount),
total_items: diviseParCent(cart.totals.total_items),
total_price: diviseParCent(cart.totals.total_items) - diviseParCent(cart.totals.total_discount)
+ newShippingPrice,
total_shipping: newShippingPrice,
};
logger.debug("ADRESSES", "initShippingCalculationButton", "newTotals", newTotals);
window.dispatchEvent(createUpdatedTotalsEvent(newTotals));
// Affiche les nouvelles Méthodes à l'Utilisateur
genereHtmlMethodesLivraison(E.CONTENEUR_METHODES_LIVRAISON, updatedMethods);
// Réinitialise le Message affiché à l'Utiisateur
E.MESSAGE_ADRESSES.textContent = " ";
// Active le Bouton pour la création de la Commande
@ -206,10 +202,12 @@ export const initialiseBoutonCalculLivraison = (): void => {
match(err)
.with(P.instanceOf(ValiError), (e: ValiError<AnySchema>): void => {
reporteEtJournaliseErreur(e);
console.error(e.issues);
E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(ErreurAdresseInvalide), (e: ErreurAdresseInvalide): void => {
reporteEtJournaliseErreur(e);
// TODO: Créer une fonction pour traiter les cas d'erreurs spécifiques
match(e.problemes)
.when(
// TODO: Créer une fonction utilitaire
@ -236,158 +234,103 @@ export const initialiseBoutonCalculLivraison = (): void => {
E.BOUTON_ACTIONS_FORMULAIRE.textContent = "Submit your addresses";
},
)
.finally((): void => {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
majEtatChargementBouton(E.BOUTON_ACTIONS_FORMULAIRE, false);
})
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
.finally((): void => setButtonLoadingState(E.BOUTON_ACTIONS_FORMULAIRE, false))
.run();
})
.ifNothing((): void => event.preventDefault());
});
};
export const initialiseBoutonCreationCommande = (): void => {
// Créé la Commande au clic sur le Bouton
export const initOrderCreationButton = (): void => {
logger.debug("ADRESSES", "initOrderCreationButton");
// Créé la Commande au clic sur le Bouton de soumission du Formulaire
E.BOUTON_ACTIONS_FORMULAIRE.addEventListener("click", (event: Event): void => {
logger.info("ADRESSES", "Demande de création de commande");
Maybe
// Ne fais rien si le Formulaire n'est pas valide
.fromFalsy(
E.FORMULAIRE_PANIER.checkValidity() && E.BOUTON_ACTIONS_FORMULAIRE.hasAttribute(ATTRIBUT_LIVRAISON_VALIDEE),
)
// Active l'état de chargement
.ifJust((): void => {
event.preventDefault();
/** Les données du Formulaire sans caractères vides. */
const donneesFormulaire: Record<string, string> = pipe(
const formFields: Record<string, string> = pipe(
Object.fromEntries(new FormData(E.FORMULAIRE_PANIER)) as Record<string, string>,
dictMap(stringTrim),
);
/** La méthode de livraison sélectionnée ; interrompt la requête si incorrect. */
const methodeLivraison: WCStoreShippingRateShippingRate = getAndParseLocalStorage(
"shipping_rates",
WCStoreShippingRateShippingRatesSchema,
)
.ifLeft((erreur: NonExistingKeyError | SyntaxError | ValiError<AnySchema>): void => {
match(erreur)
.with(P.instanceOf(ValiError), (e): void => {
reporteErreur(e);
console.error(e.issues);
})
.otherwise(reporteEtJournaliseErreur);
localStorage.removeItem("shipping_rates");
const selectedRateLS: WCStoreShippingRateShippingRate = getShippingRatesLS()
.ifNothing((): void => {
// Rétablis le Formulaire à son état d'origine
E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
// Interrompt la requête
leveErreur(erreur);
throw new Error("LocalStorage indisponible.");
})
.toMaybe()
.chainNullable((methodes: WCStoreShippingRateShippingRates) => methodes.at(0))
.chain(first)
.orDefault({});
logger.debug("ADRESSES", "initOrderCreationButton", "selectedRateLS", selectedRateLS);
/** Les Produits du Panier. */
const produitsPanier = E.ENTREES_PANIER_EITHER
const cartProducts = E.ENTREES_PANIER
.orDefault([])
.map((entree: HTMLElement) => ({
product_id: Number(entree.getAttribute("data-id-produit")),
quantity: Number(entree.getAttribute("data-quantite")),
...(entree.getAttribute("data-id-variation") && {
variation_id: Number(entree.getAttribute("data-id-variation")),
.map((entry: HTMLElement) => ({
product_id: Number(entry.getAttribute("data-id-produit")),
quantity: Number(entry.getAttribute("data-quantite")),
...(entry.getAttribute("data-id-variation") && {
variation_id: Number(entry.getAttribute("data-id-variation")),
}),
}));
logger.debug("ADRESSES", "initOrderCreationButton", "cartProducts", cartProducts);
/** Les données du Formulaire transformées en arguments pour la requête vers le Backend. */
const argumentsFormulaire: WCV3OrdersArgs = {
billing: {
address_1: donneesFormulaire["facturation-adresse"] ?? donneesFormulaire["livraison-adresse"] ?? "",
address_2: "",
city: donneesFormulaire["facturation-ville"] ?? donneesFormulaire["livraison-ville"] ?? "",
company: "",
country: E.BOUTON_SEPARATION_ADRESSES.checked
? donneesFormulaire["facturation-pays"] ?? ""
: donneesFormulaire["livraison-pays"] ?? "",
email: donneesFormulaire["facturation-email"] ?? donneesFormulaire["livraison-email"] ?? "",
first_name: donneesFormulaire["facturation-prenom"] ?? donneesFormulaire["livraison-prenom"] ?? "",
last_name: donneesFormulaire["facturation-nom"] ?? donneesFormulaire["livraison-nom"] ?? "",
phone: donneesFormulaire["facturation-telephone"] ?? donneesFormulaire["livraison-telephone"] ?? "",
postcode: donneesFormulaire["facturation-code-postal"] ?? donneesFormulaire["livraison-code-postal"] ?? "",
state: donneesFormulaire["facturation-region-etat"] ?? donneesFormulaire["livraison-region-etat"] ?? "",
},
currency: methodeLivraison.currency_code,
/** Les données du Formulaire transformées pour la requête vers le Backend. */
const formArgs: WCV3OrdersArgs = {
...getAddressesFromForm(formFields, E.BOUTON_SEPARATION_ADRESSES.checked),
currency: selectedRateLS.currency_code,
customer_note: E.INSTRUCTIONS_CLIENT.value,
line_items: produitsPanier,
shipping: {
address_1: donneesFormulaire["livraison-adresse"] ?? "",
address_2: "",
city: donneesFormulaire["livraison-ville"] ?? "",
company: "",
country: donneesFormulaire["livraison-pays"] ?? "",
first_name: donneesFormulaire["livraison-prenom"] ?? "",
last_name: donneesFormulaire["livraison-nom"] ?? "",
phone: donneesFormulaire["livraison-telephone"] ?? "",
postcode: donneesFormulaire["livraison-code-postal"] ?? "",
state: donneesFormulaire["livraison-region-etat"] ?? "",
},
line_items: cartProducts,
shipping_lines: [
{
method_id: methodeLivraison.method_id,
method_title: methodeLivraison.name,
total: pipe(diviseParCent(methodeLivraison.price), String),
method_id: selectedRateLS.method_id,
method_title: selectedRateLS.name,
total: pipe(diviseParCent(selectedRateLS.price), String),
},
],
};
logger.debug("ADRESSES", "initOrderCreationButton", "formArgs", formArgs);
// Réalise la requête et traite sa réponse
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(eitherValiParse(argumentsFormulaire, WCV3OrdersArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement
.ifRight((): void => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
majEtatChargementBouton(E.BOUTON_ACTIONS_FORMULAIRE, true);
})
// 3. Exécute la requête via fetch sous form d'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) =>
eitherAsyncFetch(
postBackend({
authString: ETATS_PAGE.authString,
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_NOUVELLE_COMMANDES,
}),
)
safeFetch(postBackend(ROUTE_API_NOUVELLE_COMMANDES, JSON.stringify(args), true))
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
.chain((rs: Response) =>
EitherAsync<HttpCodeErrors, unknown>(async ({ throwE }): Promise<unknown> =>
match(await creeReponseSimplifiee(reponse))
match(await newPartialResponse(rs))
.with({ status: 201 }, (rs): unknown => rs.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs)))
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(eitherValiParse(corps, WCV3OrderSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((commande: WCV3Order): void => {
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCV3OrderSchema)))
.ifRight((order: WCV3Order): void => {
E.BOUTON_ACTIONS_FORMULAIRE.removeAttribute(ATTRIBUT_CHARGEMENT);
E.BOUTON_ACTIONS_FORMULAIRE.textContent = "OK!";
E.MESSAGE_ADRESSES.textContent = " ";
// Redirige vers Stripe
// Maybe
// .fromNullable(new URL(`https://${window.location.host}/checkout`))
// .ifJust(url => url.searchParams.append("order_key", commande.order_key))
// .ifJust(url => url.searchParams.append("order_id", String(commande.id)))
// .ifJust(url => location.assign(url));
Maybe
.fromNullable(new URL(`https://${window.location.host}/checkout`))
.ifJust(url => url.searchParams.append("order_key", order.order_key))
.ifJust(url => url.searchParams.append("order_id", String(order.id)))
.ifJust(url => location.assign(url));
})
// 7. Traite les Erreurs et affiche un message à l'Utilisateur
.ifLeft((erreur: FetchErrors | HttpCodeErrors | ValiError<AnySchema>): void => {
match(erreur)
.ifLeft((err: FetchErrors | HttpCodeErrors | ValiError<AnySchema>): void => {
match(err)
.with(P.instanceOf(ValiError), (e: ValiError<AnySchema>): void => {
reporteEtJournaliseErreur(e);
console.error(e.issues);
@ -404,7 +347,7 @@ export const initialiseBoutonCreationCommande = (): void => {
.exhaustive();
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
majEtatChargementBouton(E.BOUTON_ACTIONS_FORMULAIRE, false);
setButtonLoadingState(E.BOUTON_ACTIONS_FORMULAIRE, false);
E.BOUTON_ACTIONS_FORMULAIRE.textContent = "Checkout";
})
.run();

View file

@ -10,8 +10,8 @@ import { ValiError } from "valibot";
import type { WCStoreCart } from "../lib/types/api/cart";
import type { WCStoreCartApplyCouponArgs } from "../lib/types/api/cart-apply-coupon";
import type { WCStoreCartRemoveCouponArgs } from "../lib/types/api/cart-remove-coupon";
import type { EtatsPageGenerique } from "../lib/types/pages";
import type { ReponseSimplifiee } from "../lib/types/reseau";
import type { GenericPageState } from "../lib/types/pages";
import type { SimplifiedResponse } from "../lib/types/reseau";
import { ROUTE_API_APPLIQUE_COUPON, ROUTE_API_RETIRE_COUPON } from "../constantes/api";
import { ERREUR_CODE_PROMO_INVALIDE } from "../constantes/api/erreurs";
@ -21,16 +21,10 @@ import {
ATTRIBUT_DESACTIVE,
ATTRIBUT_HIDDEN,
SELECTEUR_BOUTON_CODE_PROMO,
SELECTEUR_CHAMP_CODE_PROMO,
SELECTEUR_ENSEMBLE_CODE_PROMO,
SELECTEUR_MESSAGE_CODE_PROMO,
SELECTEUR_TOTAL_PANIER,
SELECTEUR_TOTAL_REDUCTION,
SELECTEUR_TOTAL_REDUCTION_VALEUR,
} from "../constantes/dom";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages";
import { lanceAnimationCycleLoading } from "../lib/animations";
import { accorderCibleASelecteur } from "../lib/dom";
import { targetMatchesSelector } from "../lib/dom";
import { reporteErreur, ServerError } from "../lib/erreurs";
import { ErreurCodePromoInvalide } from "../lib/erreurs/codes-promo";
import { CODE_PROMO_MAJ_EVENT } from "../lib/evenements/panier";
@ -41,22 +35,12 @@ import { postBackend } from "../lib/reseau";
import { WCStoreCartSchema } from "../lib/schemas/api/cart";
import { WCStoreCartApplyCouponArgsSchema } from "../lib/schemas/api/cart-apply-coupon";
import { WCStoreCartRemoveCouponArgsSchema } from "../lib/schemas/api/cart-remove-coupon";
import { recupereEleOuLeve } from "../lib/utils";
import { eitherValiParse } from "../lib/validation";
import { safeSchemaParse } from "../lib/validation";
import { E } from "./scripts-page-panier-elements";
// @ts-expect-error -- États injectés par le modèle PHP
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPageGenerique = _etats;
const E = {
BOUTON_CODE_PROMO: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_CODE_PROMO),
CHAMP_CODE_PROMO: recupereEleOuLeve<HTMLInputElement>(SELECTEUR_CHAMP_CODE_PROMO),
ENSEMBLE_CODE_PROMO: recupereEleOuLeve<HTMLFormElement>(SELECTEUR_ENSEMBLE_CODE_PROMO),
MESSAGE_CODE_PROMO: recupereEleOuLeve<HTMLParagraphElement>(SELECTEUR_MESSAGE_CODE_PROMO),
TOTAL_PANIER: recupereEleOuLeve<HTMLParagraphElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_REDUCTION_LIGNE: recupereEleOuLeve<HTMLDivElement>(SELECTEUR_TOTAL_REDUCTION),
TOTAL_REDUCTION_VALEUR: recupereEleOuLeve<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
};
const ETATS_PAGE: GenericPageState = _etats;
export const initialiseElementsCodePromo = (): void => {
const recupereValeurCodePromo = (): null | string =>
@ -75,7 +59,7 @@ export const initialiseElementsCodePromo = (): void => {
.with(
{
cible: P.when((cible: EventTarget | null) =>
accorderCibleASelecteur<HTMLButtonElement>(cible, SELECTEUR_BOUTON_CODE_PROMO)
targetMatchesSelector<HTMLButtonElement>(cible, SELECTEUR_BOUTON_CODE_PROMO)
),
codePromoPresent: false,
valeurCodePromo: P.string,
@ -83,7 +67,7 @@ export const initialiseElementsCodePromo = (): void => {
({ valeurCodePromo }) =>
void EitherAsync
// Vérifie le Schéma des arguments
.liftEither(eitherValiParse({ code: valeurCodePromo }, WCStoreCartApplyCouponArgsSchema))
.liftEither(safeSchemaParse({ code: valeurCodePromo }, WCStoreCartApplyCouponArgsSchema))
.ifRight(() => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_DESACTIVE, "");
@ -105,7 +89,7 @@ export const initialiseElementsCodePromo = (): void => {
// Traite les cas d'Erreur
.chain((reponse: Response) =>
EitherAsync<ErreurCodePromoInvalide | ServerError, unknown>(async ({ throwE }) => {
const reponseSimplifiee: ReponseSimplifiee = {
const reponseSimplifiee: SimplifiedResponse = {
body: await reponse.json(),
status: reponse.status,
};
@ -121,7 +105,7 @@ export const initialiseElementsCodePromo = (): void => {
})
)
// Vérifie le Schéma de la Réponse du backend
.chain((corpsReponse: unknown) => EitherAsync.liftEither(eitherValiParse(corpsReponse, WCStoreCartSchema)))
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
// Déclenche les mises à jour du DOM avec les données du nouveau Panier
.ifRight((panier: WCStoreCart) => {
E.ENSEMBLE_CODE_PROMO.toggleAttribute(ATTRIBUT_CODE_PROMO_PRESENT);
@ -186,13 +170,13 @@ export const initialiseElementsCodePromo = (): void => {
// Un code promo est présent sous forme de chaîne
.with(
{
cible: P.when(cible => accorderCibleASelecteur<HTMLButtonElement>(cible, SELECTEUR_BOUTON_CODE_PROMO)),
cible: P.when(cible => targetMatchesSelector<HTMLButtonElement>(cible, SELECTEUR_BOUTON_CODE_PROMO)),
codePromoPresent: true,
valeurCodePromo: P.string,
},
({ valeurCodePromo }) =>
void EitherAsync
.liftEither(eitherValiParse({ code: valeurCodePromo }, WCStoreCartRemoveCouponArgsSchema))
.liftEither(safeSchemaParse({ code: valeurCodePromo }, WCStoreCartRemoveCouponArgsSchema))
.ifRight(() => {
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_CHARGEMENT, "");
@ -212,7 +196,7 @@ export const initialiseElementsCodePromo = (): void => {
return await reponse.json();
})
)
.chain((corpsReponse: unknown) => EitherAsync.liftEither(eitherValiParse(corpsReponse, WCStoreCartSchema)))
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
.ifRight((panier: WCStoreCart) => {
E.ENSEMBLE_CODE_PROMO.toggleAttribute(ATTRIBUT_CODE_PROMO_PRESENT);
E.ENSEMBLE_CODE_PROMO.reset();

View file

@ -0,0 +1,48 @@
import {
SELECTEUR_BOUTON_ACTIONS_FORMULAIRE,
SELECTEUR_BOUTON_CODE_PROMO,
SELECTEUR_BOUTON_SEPARATION_ADRESSES,
SELECTEUR_CHAMP_CODE_PROMO,
SELECTEUR_CONTENEUR_METHODES_LIVRAISON,
SELECTEUR_CONTENEUR_PANIER,
SELECTEUR_ENSEMBLE_CODE_PROMO,
SELECTEUR_ENTREES_PANIER,
SELECTEUR_FORMULAIRE_FACTURATION,
SELECTEUR_FORMULAIRE_PANIER,
SELECTEUR_INSTRUCTIONS_CLIENT,
SELECTEUR_MESSAGE_CODE_PROMO,
SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES,
SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT,
SELECTEUR_SOUS_TOTAL_PRODUITS,
SELECTEUR_TOTAL_PANIER,
SELECTEUR_TOTAL_REDUCTION,
SELECTEUR_TOTAL_REDUCTION_VALEUR,
} from "../constantes/dom";
import { mustGetEleInDocument, recupereElementsDocumentEither } from "../lib/dom";
export const E = {
BOUTON_ACTIONS_FORMULAIRE: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_ACTIONS_FORMULAIRE),
BOUTON_CODE_PROMO: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_CODE_PROMO),
BOUTON_SEPARATION_ADRESSES: mustGetEleInDocument<HTMLInputElement>(SELECTEUR_BOUTON_SEPARATION_ADRESSES),
CHAMP_CODE_PROMO: mustGetEleInDocument<HTMLInputElement>(SELECTEUR_CHAMP_CODE_PROMO),
CONTENEUR_METHODES_LIVRAISON: mustGetEleInDocument<HTMLFieldSetElement>(SELECTEUR_CONTENEUR_METHODES_LIVRAISON),
CONTENEUR_PANIER: mustGetEleInDocument<HTMLElement>(SELECTEUR_CONTENEUR_PANIER),
ENSEMBLE_CODE_PROMO: mustGetEleInDocument<HTMLFormElement>(SELECTEUR_ENSEMBLE_CODE_PROMO),
ENTREES_PANIER: recupereElementsDocumentEither<HTMLElement>(
SELECTEUR_ENTREES_PANIER,
),
FORMULAIRE_FACTURATION: mustGetEleInDocument<HTMLDivElement>(SELECTEUR_FORMULAIRE_FACTURATION),
FORMULAIRE_PANIER: mustGetEleInDocument<HTMLFormElement>(SELECTEUR_FORMULAIRE_PANIER),
INSTRUCTIONS_CLIENT: mustGetEleInDocument<HTMLTextAreaElement>(SELECTEUR_INSTRUCTIONS_CLIENT),
MESSAGE_ADRESSES: mustGetEleInDocument<HTMLParagraphElement>(SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES),
MESSAGE_CODE_PROMO: mustGetEleInDocument<HTMLParagraphElement>(SELECTEUR_MESSAGE_CODE_PROMO),
SOUS_TOTAL_LIVRAISON_VALEUR: mustGetEleInDocument<HTMLElement>(SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT),
SOUS_TOTAL_PRODUITS: mustGetEleInDocument<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_PRODUITS_VALEUR: mustGetEleInDocument<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_REDUCTION: mustGetEleInDocument<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
SOUS_TOTAL_REDUCTION_VALEUR: mustGetEleInDocument<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
TOTAL_PANIER: mustGetEleInDocument<HTMLParagraphElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_PANIER_VALEUR: mustGetEleInDocument<HTMLSpanElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_REDUCTION_LIGNE: mustGetEleInDocument<HTMLDivElement>(SELECTEUR_TOTAL_REDUCTION),
TOTAL_REDUCTION_VALEUR: mustGetEleInDocument<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
};

View file

@ -1,28 +1,15 @@
import { Either } from "purify-ts";
import type { MethodesLivraisonMajEvent, TotauxMajEvent } from "../lib/evenements/panier";
import type { UpdatedShippingRatesEvent, UpdatedTotalsEvent } from "../lib/evenements/panier";
import {
ATTRIBUT_LIVRAISON_VALIDEE,
SELECTEUR_BOUTON_ACTIONS_FORMULAIRE,
SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT,
SELECTEUR_SOUS_TOTAL_PRODUITS,
SELECTEUR_TOTAL_PANIER,
SELECTEUR_TOTAL_REDUCTION_VALEUR,
} from "../constantes/dom";
import { ADRESSES_MAJ, CODE_PROMO_MAJ, METHODES_LIVRAISON_MAJ, TOTAUX_MAJ } from "../constantes/evenements";
import { ATTRIBUT_LIVRAISON_VALIDEE } from "../constantes/dom";
import { ADRESSES_MAJ, CODE_PROMO_MAJ, SHIPPING_RATES_UPDATED, TOTALS_UPDATED } from "../constantes/evenements";
import { reporteEtJournaliseErreur } from "../lib/erreurs";
import { setLocalStorage } from "../lib/local-storage";
import { eitherSetLocalStorage } from "../lib/local-storage";
import { formateEnEuros } from "../lib/nombres";
import { recupereEleOuLeve } from "../lib/utils";
const E = {
BOUTON_ACTIONS_FORMULAIRE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_ACTIONS_FORMULAIRE),
SOUS_TOTAL_LIVRAISON_VALEUR: recupereEleOuLeve<HTMLElement>(SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT),
SOUS_TOTAL_PRODUITS_VALEUR: recupereEleOuLeve<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_REDUCTION_VALEUR: recupereEleOuLeve<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
TOTAL_PANIER_VALEUR: recupereEleOuLeve<HTMLSpanElement>(SELECTEUR_TOTAL_PANIER),
};
import { logger } from "../logging";
import { E } from "./scripts-page-panier-elements";
import { generateShippingRatesHTML } from "./scripts-page-panier-methodes-livraison";
/**
* Réinitialise le bouton de soumission du Formulaire en revenant à l'étape de validation des adresses.
@ -48,23 +35,29 @@ export const souscrisEvenementsPanier = (): void => {
reinitialiseValidationLivraison();
});
window.addEventListener(METHODES_LIVRAISON_MAJ, (event: Event): void => {
window.addEventListener(SHIPPING_RATES_UPDATED, (event: Event): void => {
Either
// La vérification du schéma se fait à l'émission
.encase(() => (event as MethodesLivraisonMajEvent).detail.methodes)
.chain(ms => setLocalStorage("shipping_rates", ms))
.ifLeft(reporteEtJournaliseErreur)
.encase(() => (event as UpdatedShippingRatesEvent).detail)
// Met à jour le DOM
.ifRight(ms => console.debug("methods", ms))
.toMaybe()
.chainNullable(xs => xs.find(m => m.selected));
.ifRight(event => {
logger.info("ShippingRatesUpdatedEvent", "shipping_rates", event);
// Met à jour les Méthodes à l'Utilisateur si demandé
// Il peut y en avoir aucune
if (event.refresh_methods) {
generateShippingRatesHTML(E.CONTENEUR_METHODES_LIVRAISON, event.shipping_rates);
}
})
// Met à jour le LocalStorage
.chain(event => eitherSetLocalStorage("shipping_rates", event.shipping_rates))
.ifLeft(reporteEtJournaliseErreur);
});
window.addEventListener(TOTAUX_MAJ, (event: Event): void => {
window.addEventListener(TOTALS_UPDATED, (event: Event): void => {
Either
// La vérification du Schéma se fait à l'émission
.encase(() => (event as TotauxMajEvent).detail.totaux)
.chain(ts => setLocalStorage("totals", ts))
.encase(() => (event as UpdatedTotalsEvent).detail.totals)
.chain(ts => eitherSetLocalStorage("totals", ts))
.ifLeft(reporteEtJournaliseErreur)
// Met à jour le DOM
.ifRight(ts => {

View file

@ -0,0 +1,21 @@
import type { Maybe } from "purify-ts";
import type { WCStoreShippingRateShippingRates } from "../lib/types/api/couts-livraison";
import { reporteEtJournaliseErreur } from "../lib/erreurs";
import { getLocalStorageByKey, setLocalStorageByKey } from "../lib/local-storage";
import { WCStoreShippingRateShippingRatesSchema } from "../lib/schemas/api/couts-livraison";
/* LS = LocalStorage */
export const getShippingRatesLS = (): Maybe<WCStoreShippingRateShippingRates> =>
getLocalStorageByKey("shipping_rates", WCStoreShippingRateShippingRatesSchema)
.ifLeft(reporteEtJournaliseErreur)
.toMaybe();
export const setShippingRatesLS = (
shippingRates: WCStoreShippingRateShippingRates,
): Maybe<WCStoreShippingRateShippingRates> =>
setLocalStorageByKey("shipping_rates", WCStoreShippingRateShippingRatesSchema)(shippingRates)
.ifLeft(reporteEtJournaliseErreur)
.toMaybe();

View file

@ -1,81 +1,73 @@
import { find as arrayFind, forEach as arrayForEach, map as arrayMap } from "@mobily/ts-belt/Array";
import { forEach as arrayForEach, map as arrayMap } from "@mobily/ts-belt/Array";
import { html, render, type TemplateResult } from "lit-html";
import type { WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreCartTotals, WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreShippingRateShippingRates } from "../lib/types/api/couts-livraison";
import { ATTRIBUT_HIDDEN, SELECTEUR_CONTENEUR_METHODES_LIVRAISON } from "../constantes/dom";
import { recupereElementsAvecSelecteur } from "../lib/dom";
import { ATTRIBUT_HIDDEN } from "../constantes/dom";
import { forEach, map } from "../lib/arrays";
import { getDOMElementsWithSelector } from "../lib/dom";
import { reporteEtJournaliseErreur } from "../lib/erreurs";
import { MethodesLivraisonMajEvent, TotauxMajEvent } from "../lib/evenements/panier";
import { getAndParseLocalStorage } from "../lib/local-storage";
import { createUpdatedShippingRatesEvent, createUpdatedTotalsEvent } from "../lib/evenements/panier";
import { getLocalStorageByKey } from "../lib/local-storage";
import { formateEnEuros } from "../lib/nombres";
import { find } from "../lib/safe-arrays";
import { WCStoreCartTotalsSchema } from "../lib/schemas/api/cart";
import { WCStoreShippingRateShippingRatesSchema } from "../lib/schemas/api/couts-livraison";
import { recupereEleOuLeve } from "../lib/utils";
import { logger } from "../logging";
import { E } from "./scripts-page-panier-elements";
import { getShippingRatesLS } from "./scripts-page-panier-local-storage";
const E = {
CONTENEUR_METHODES_LIVRAISON: recupereEleOuLeve<HTMLFieldSetElement>(SELECTEUR_CONTENEUR_METHODES_LIVRAISON),
};
export const initShippingRatesChoicesActions = (): void => {
logger.debug("METHODES_LIVRAISON", "initShippingRatesChoicesActions");
getDOMElementsWithSelector(E.CONTENEUR_METHODES_LIVRAISON)<HTMLInputElement>("input")
.ifRight(forEach((el: HTMLInputElement): void =>
el.addEventListener("click", (event: MouseEvent): void => {
logger.info("METHODES_LIVRAISON", "Clic sur un choix de méthode de livraison");
/**
* Créé un Événement MethodesLivraisonMaj déclenché quand l'Utilisateur change son choix avec en corps un object ShippingRates
* À l'Émission, sauvegarde les méthodes mises à jour dans le LocalStorage et met à jour le DOM
* Le déclenchement de la mise à jour des adresses déclenche aussi cet Événement.
* Le LocalStorage est comparé avec la Réponse, et si la méthode jusqe- sélectionnée est présente dans la Réponse, la mise à jour du DOM doit se faire en conservant ce choix.
*/
export const initialiseBoutonsChoixMethodesLivraison = (): void => {
recupereElementsAvecSelecteur(E.CONTENEUR_METHODES_LIVRAISON)<HTMLInputElement>("input").ifRight(
arrayForEach((i): void =>
i.addEventListener("click", (evenement: MouseEvent): void => {
getAndParseLocalStorage("shipping_rates", WCStoreShippingRateShippingRatesSchema)
.ifLeft(reporteEtJournaliseErreur)
.map(arrayMap(m => {
// Met à jour la Méthode sélectionnée
m.selected = m.method_id === (evenement.target as HTMLInputElement).value;
return m;
// Récupère les méthodes du LocalStorage et les met à jour avec le nouveau choix
getShippingRatesLS()
// Met à jour la sélection de la Méthode
.map(map((sr: WCStoreShippingRateShippingRate) => {
sr.selected = sr.method_id === (event.target as HTMLInputElement).value;
return sr;
}))
// Émet l'Événement de màj des Méthodes
.ifRight(xs => window.dispatchEvent(MethodesLivraisonMajEvent(xs)))
.toMaybe()
.chainNullable(arrayFind(m => m.selected))
.ifJust(m => {
// Émet l'Événement de màj des Totaux
getAndParseLocalStorage("totals", WCStoreCartTotalsSchema)
// Met à jour les Méthodes de livraison dans le LocalStorage et le DOM
.ifJust((srs: WCStoreShippingRateShippingRates): void => {
window.dispatchEvent(createUpdatedShippingRatesEvent(srs, false));
})
// Met à jour les totaux dans le LocalStorage et le DOM
.chain(find(sr => sr.selected))
.ifJust((sr: WCStoreShippingRateShippingRate): void => {
getLocalStorageByKey("totals", WCStoreCartTotalsSchema)
.ifLeft(reporteEtJournaliseErreur)
.map(ts => {
ts.total_shipping = m.price;
ts.total_price = ts.total_items - ts.total_discount + m.price;
.map((ts: WCStoreCartTotals): WCStoreCartTotals => {
ts.total_shipping = sr.price;
ts.total_price = ts.total_items - ts.total_discount + sr.price;
return ts;
})
.ifRight(t => window.dispatchEvent(TotauxMajEvent(t)));
.ifRight((ts: WCStoreCartTotals): void => {
window.dispatchEvent(createUpdatedTotalsEvent(ts));
});
});
})
),
);
));
};
export const genereHtmlMethodesLivraison = (
conteneur: HTMLElement,
methodes: ReadonlyArray<WCStoreShippingRateShippingRate>,
export const generateShippingRatesHTML = (
container: HTMLElement,
shippingRates: ReadonlyArray<WCStoreShippingRateShippingRate>,
): void => {
// Cache les méthodes s'il n'y en a pas
if (methodes.length === 0) {
conteneur.setAttribute(ATTRIBUT_HIDDEN, "");
// Cache les méthodes s'il n'y en a pas sans continuer
if (shippingRates.length === 0) {
container.setAttribute(ATTRIBUT_HIDDEN, "");
return;
}
// Retire les méthodes de livraison initiales
recupereElementsAvecSelecteur(conteneur)("div[data-methode-initiale]").ifRight(arrayForEach(div => div.remove()));
getDOMElementsWithSelector(container)("div[data-methode-initiale]").ifRight(arrayForEach(div => div.remove()));
const methodeSelectionnee: string = getAndParseLocalStorage("shipping_rates", WCStoreShippingRateShippingRatesSchema)
.ifLeft(reporteEtJournaliseErreur)
.toMaybe()
.chainNullable(arrayFind(m => m.selected))
.map(m => m.method_id)
.orDefault("");
const methodesHtml: ReadonlyArray<TemplateResult> = arrayMap(methodes, methode => {
const selectedShippingRate: string = shippingRates.find(sr => sr.selected)?.method_id ?? "";
const shippingRatesHTML: ReadonlyArray<TemplateResult> = arrayMap(shippingRates, methode => {
return html`
<div>
<input
@ -83,15 +75,15 @@ export const genereHtmlMethodesLivraison = (
name="choix-methode-livraison"
type="radio"
value="${methode.method_id}"
.checked="${methode.method_id === methodeSelectionnee}"
.checked="${methode.method_id === selectedShippingRate}"
>
<label for="methode-livraison-${methode.method_id}">${methode.name} (${formateEnEuros(methode.price)})</label>
</div>`;
});
// Ajoute les nouveaux Produits dans le DOM
conteneur.removeAttribute(ATTRIBUT_HIDDEN);
render(methodesHtml, conteneur);
container.removeAttribute(ATTRIBUT_HIDDEN);
render(shippingRatesHTML, container);
// Recréé les Écouteurs de clic sur les choix de méthodes
initialiseBoutonsChoixMethodesLivraison();
initShippingRatesChoicesActions();
};

View file

@ -9,9 +9,8 @@ import { type AnySchema, ValiError } from "valibot";
import type { WCStoreCart } from "../lib/types/api/cart";
import type { WCStoreCartRemoveItemArgs } from "../lib/types/api/cart-remove-item";
import type { WCStoreCartUpdateItemArgs } from "../lib/types/api/cart-update-item";
import type { ElementParent } from "../lib/types/dom";
import type { EtatsPageGenerique } from "../lib/types/pages";
import type { FetchErrors } from "../lib/types/reseau";
import type { GenericPageState } from "../lib/types/pages";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau";
import { ROUTE_API_MAJ_ARTICLE_PANIER, ROUTE_API_RETIRE_ARTICLE_PANIER } from "../constantes/api";
import {
@ -21,10 +20,9 @@ import {
SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE,
SELECTEUR_BOUTON_SUPPRESSION_PANIER,
SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER,
SELECTEUR_ENTREES_PANIER,
} from "../constantes/dom";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages";
import { recupereElementAvecSelecteur, recupereElementOuLeve } from "../lib/dom";
import { mustGetEleInParent } from "../lib/dom";
import { BadRequestError, reporteErreur, ServerError } from "../lib/erreurs";
import {
emetMessageMajBoutonPanier,
@ -32,164 +30,146 @@ import {
emetUniqueMessageBroadcastChannel,
} from "../lib/messages";
import { diviseParCent } from "../lib/nombres";
import { creeReponseSimplifiee, eitherAsyncFetch, postBackend } from "../lib/reseau";
import { newPartialResponse, postBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau";
import { WCStoreCartSchema } from "../lib/schemas/api/cart";
import { WCStoreCartRemoveItemArgsSchema } from "../lib/schemas/api/cart-remove-item";
import { WCStoreCartUpdateItemArgsSchema } from "../lib/schemas/api/cart-update-item";
import { recupereElementsDocumentEither } from "../lib/utils";
import { eitherValiParse } from "../lib/validation";
import { safeSchemaParse } from "../lib/validation";
import { E } from "./scripts-page-panier-elements";
// @ts-expect-error -- États injectés par le modèle PHP
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPageGenerique = _etats;
const PAGE_STATE: GenericPageState = _etats;
type ElementsEntreePanier = {
boutonAddition: HTMLButtonElement;
boutonSoustraction: HTMLButtonElement;
boutonSuppression: HTMLButtonElement;
champQuantite: HTMLInputElement;
type CartEntryInteractiveElements = {
additionButton: HTMLButtonElement;
deletionButton: HTMLButtonElement;
quantityInput: HTMLInputElement;
substractionButton: HTMLButtonElement;
};
const E = {
ENTREES_PANIER: recupereElementsDocumentEither<HTMLElement>(SELECTEUR_ENTREES_PANIER),
};
// TODO: Tout ça est bien compliqué
const recupereEleDansEntreeOuLeve = (entree: ElementParent) => <E extends HTMLElement>(selecteur: string) =>
pipe(recupereElementAvecSelecteur(entree)<E>(selecteur), recupereElementOuLeve);
const recupereElementsEntreePanier = (entree: HTMLElement): ElementsEntreePanier => {
const recupereElementDansEntree = recupereEleDansEntreeOuLeve(entree);
const getCartEntryInteractiveEles = (entree: HTMLElement): CartEntryInteractiveElements => {
const mustGetEle = mustGetEleInParent(entree);
return {
boutonAddition: recupereElementDansEntree<HTMLButtonElement>(SELECTEUR_BOUTON_ADDITION_QUANTITE),
boutonSoustraction: recupereElementDansEntree<HTMLButtonElement>(SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE),
boutonSuppression: recupereElementDansEntree<HTMLButtonElement>(SELECTEUR_BOUTON_SUPPRESSION_PANIER),
champQuantite: recupereElementDansEntree<HTMLInputElement>(SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER),
additionButton: mustGetEle<HTMLButtonElement>(SELECTEUR_BOUTON_ADDITION_QUANTITE),
deletionButton: mustGetEle<HTMLButtonElement>(SELECTEUR_BOUTON_SUPPRESSION_PANIER),
quantityInput: mustGetEle<HTMLInputElement>(SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER),
substractionButton: mustGetEle<HTMLButtonElement>(SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE),
};
};
/**
* Met à jour l'état d'activation des Boutons d'action sur chaque Entrée du Panier.
* @param activation Le nouvel état d'activation (activé/désactivé).
* @param activated Le nouvel état d'activation (activé/désactivé).
* @returns Rien.
*/
export const majActivationBoutons = (activation: boolean) => (entrees: ReadonlyArray<ElementsEntreePanier>): void =>
arrayForEach(entrees, (entree: ElementsEntreePanier): void => {
if (activation) {
// Active les Boutons
Number(entree.champQuantite.value) === 1
? entree.boutonSoustraction.setAttribute(ATTRIBUT_DESACTIVE, "")
: entree.boutonSoustraction.removeAttribute(ATTRIBUT_DESACTIVE);
entree.boutonAddition.removeAttribute(ATTRIBUT_DESACTIVE);
entree.boutonSuppression.removeAttribute(ATTRIBUT_DESACTIVE);
entree.boutonSuppression.textContent = "Remove";
} else {
// Désactive les Boutons
entree.boutonSoustraction.setAttribute(ATTRIBUT_DESACTIVE, "");
entree.boutonAddition.setAttribute(ATTRIBUT_DESACTIVE, "");
entree.boutonSuppression.setAttribute(ATTRIBUT_DESACTIVE, "");
entree.boutonSuppression.textContent = "Loading";
}
});
export const toggleCartEntryButtons =
(activated: boolean) => (cartEntries: ReadonlyArray<CartEntryInteractiveElements>): void =>
arrayForEach(cartEntries, (e: CartEntryInteractiveElements): void => {
if (activated) {
// Active les Boutons
Number(e.quantityInput.value) === 1
? e.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "")
: e.substractionButton.removeAttribute(ATTRIBUT_DESACTIVE);
e.additionButton.removeAttribute(ATTRIBUT_DESACTIVE);
e.deletionButton.removeAttribute(ATTRIBUT_DESACTIVE);
e.deletionButton.textContent = "Remove";
} else {
// Désactive les Boutons
e.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
e.additionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
e.deletionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
e.deletionButton.textContent = "Loading";
}
});
export const initialiseActionsEntreesPanier = (): void => {
// Initialise des actions uniquement si des Entrées dans le Panier existent
E.ENTREES_PANIER.ifRight((entrees: Array<HTMLElement>) =>
arrayForEach(entrees, (entree: HTMLElement): void => {
/** Retire l'entrée du DOM si la clé Panier n'existe pas et arrête précocement */
const clePanier: string = Maybe
.fromNullable(entree.getAttribute(ATTRIBUT_CLE_PANIER))
.ifNothing(() => {
entree.remove();
})
E.ENTREES_PANIER.ifRight((cartEntries: Array<HTMLElement>) =>
arrayForEach(cartEntries, (entry: HTMLElement): void => {
// Retire l'entrée du DOM si la clé Panier n'existe pas puis arrête précocement
const entryKey: string = Maybe
.fromNullable(entry.getAttribute(ATTRIBUT_CLE_PANIER))
.ifNothing(() => entry.remove())
.orDefault("CLE_PANIER_INEXISTANTE");
/** */
const E_P: ElementsEntreePanier = recupereElementsEntreePanier(entree);
const entryButtons: CartEntryInteractiveElements = getCartEntryInteractiveEles(entry);
entree.addEventListener("click", (evenement: Event): void => {
// Délégation d'Événements
match(evenement.target)
.with(P.nullish, () => console.error(evenement.target))
// Bouton d'addition
entry.addEventListener("click", (event: Event): void => {
// Discrimine en fonction de l'Élément cliqué (délégation d'Événements)
match(event.target)
// Cas impossible
.with(P.nullish, () => console.error(event.target))
// Clic sur le Bouton d'addition
.when(
(cible: EventTarget) => (cible as HTMLElement).matches(SELECTEUR_BOUTON_ADDITION_QUANTITE),
(target: EventTarget) => (target as HTMLElement).matches(SELECTEUR_BOUTON_ADDITION_QUANTITE),
(): void => {
Maybe
// Nécessaire pour que l'on ait une valeur à incrémenter
.fromNullable(E_P.champQuantite.valueAsNumber)
.ifJust((valeur: number) => {
// Réalise la requête et traite sa réponse
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(
eitherValiParse({ key: clePanier, quantity: valeur + 1 }, WCStoreCartUpdateItemArgsSchema),
)
// 2. Exécute un Effet pour empêcher les requêtes concurrentes
.ifRight(() => pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(false)))
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartUpdateItemArgs) =>
eitherAsyncFetch(postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_MAJ_ARTICLE_PANIER,
}))
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
EitherAsync<BadRequestError | Error | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
match(await creeReponseSimplifiee(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 200 }, r => r.body)
.otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`)))
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(eitherValiParse(corps, WCStoreCartSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((panier: WCStoreCart): void => {
// Émet un Message avec le nouveau nombre de Produits dans le Panier
emetMessageMajBoutonPanier({ quantiteProduits: panier.items_count });
// Émet un Message avec le nouveau contenu du Panier
emetMessageMajContenuPanier({
produits: panier.items,
sousTotalProduits: diviseParCent(panier.totals.total_items),
sousTotalReduction: diviseParCent(panier.totals.total_discount),
totalPanier: diviseParCent(panier.totals.total_price),
});
// Émet un Message pour réinitialiser la validation de la livraison
emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
})
// 7. Traite les Erreurs et affiche un message à l'Utilisateur
.ifLeft(
(erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>): void => {
match(erreur)
.with(P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
})
.exhaustive();
},
)
.finally(() => {
// Réactive les Boutons
pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(true));
})
.run();
});
void EitherAsync
.liftEither(
Maybe
// Une valeur à incrémenter doit exister
.fromNullable(entryButtons.quantityInput.valueAsNumber)
.toEither(new Error("Quantité manquante pour cette ligne du Panier !")),
)
.chain(q =>
EitherAsync.liftEither(
safeSchemaParse({ key: entryKey, quantity: q + 1 }, WCStoreCartUpdateItemArgsSchema),
)
)
.ifRight(() => pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false)))
.chain((args: WCStoreCartUpdateItemArgs) =>
safeFetch(postBackend({
corps: JSON.stringify(args),
nonce: PAGE_STATE.nonce,
route: ROUTE_API_MAJ_ARTICLE_PANIER,
}))
)
.chain((r: Response) =>
EitherAsync<ServerError, unknown>(async ({ throwE }) =>
match(await newPartialResponse(r))
.with({ status: 200 }, r => r.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs)))
)
)
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCStoreCartSchema)))
.ifRight((c: WCStoreCart): void => {
// Émet un Message avec le nouveau nombre de Produits dans le Panier
emetMessageMajBoutonPanier({ quantiteProduits: c.items_count });
// Émet un Message avec le nouveau contenu du Panier
emetMessageMajContenuPanier({
produits: c.items,
sousTotalProduits: diviseParCent(c.totals.total_items),
sousTotalReduction: diviseParCent(c.totals.total_discount),
totalPanier: diviseParCent(c.totals.total_price),
});
// Émet un Message pour réinitialiser la validation de la livraison
emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
})
.ifLeft(
(err: FetchErrors | HttpCodeErrors | ValiError<AnySchema>): void => {
match(err)
.with(P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
})
.exhaustive();
},
)
.finally(() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
.run();
},
)
// Bouton de soustraction
@ -198,22 +178,24 @@ export const initialiseActionsEntreesPanier = (): void => {
(): void => {
Maybe
// Nécessaire pour que l'on ait une valeur à incrémenter
.fromNullable(E_P.champQuantite.valueAsNumber)
.fromNullable(entryButtons.quantityInput.valueAsNumber)
.filter(valeur => valeur > 1)
.ifJust((valeur: number) => {
// Réalise la requête et traite sa réponse
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(
eitherValiParse({ key: clePanier, quantity: valeur - 1 }, WCStoreCartUpdateItemArgsSchema),
safeSchemaParse({ key: entryKey, quantity: valeur - 1 }, WCStoreCartUpdateItemArgsSchema),
)
// 2. Exécute un Effet pour empêcher les requêtes concurrentes
.ifRight(() => pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(false)))
.ifRight(() =>
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false))
)
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartUpdateItemArgs) =>
eitherAsyncFetch(postBackend({
safeFetch(postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
nonce: PAGE_STATE.nonce,
route: ROUTE_API_MAJ_ARTICLE_PANIER,
}))
)
@ -221,7 +203,7 @@ export const initialiseActionsEntreesPanier = (): void => {
.chain((reponse: Response) =>
EitherAsync<BadRequestError | Error | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
match(await creeReponseSimplifiee(reponse))
match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 200 }, r => r.body)
@ -229,7 +211,7 @@ export const initialiseActionsEntreesPanier = (): void => {
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(eitherValiParse(corps, WCStoreCartSchema)))
.chain((corps: unknown) => EitherAsync.liftEither(safeSchemaParse(corps, WCStoreCartSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((panier: WCStoreCart): void => {
// Émet un Message avec le nouveau nombre de Produits dans le Panier
@ -268,7 +250,7 @@ export const initialiseActionsEntreesPanier = (): void => {
)
.finally(() => {
// Réactive les Boutons
pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(true));
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
.run();
});
@ -280,19 +262,21 @@ export const initialiseActionsEntreesPanier = (): void => {
(): void => {
Maybe
// TODO: Pourquoi ?
.fromNullable(E_P.champQuantite.valueAsNumber)
.fromNullable(entryButtons.quantityInput.valueAsNumber)
.ifJust(() => {
// Réalise la requête et traite sa réponse
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(eitherValiParse({ key: clePanier }, WCStoreCartRemoveItemArgsSchema))
.liftEither(safeSchemaParse({ key: entryKey }, WCStoreCartRemoveItemArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes
.ifRight(() => pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(false)))
.ifRight(() =>
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false))
)
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartRemoveItemArgs) =>
eitherAsyncFetch(postBackend({
safeFetch(postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
nonce: PAGE_STATE.nonce,
route: ROUTE_API_RETIRE_ARTICLE_PANIER,
}))
)
@ -300,7 +284,7 @@ export const initialiseActionsEntreesPanier = (): void => {
.chain((reponse: Response) =>
EitherAsync<BadRequestError | Error | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
match(await creeReponseSimplifiee(reponse))
match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 200 }, r => r.body)
@ -308,7 +292,7 @@ export const initialiseActionsEntreesPanier = (): void => {
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(eitherValiParse(corps, WCStoreCartSchema)))
.chain((corps: unknown) => EitherAsync.liftEither(safeSchemaParse(corps, WCStoreCartSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((panier: WCStoreCart): void => {
// Émet un Message avec le nouveau nombre de Produits dans le Panier
@ -324,7 +308,7 @@ export const initialiseActionsEntreesPanier = (): void => {
emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
// Retire l'entrée du Panier du DOM
entree.remove();
entry.remove();
})
// 7. Traite les Erreurs et affiche un message à l'Utilisateur
.ifLeft(
@ -350,13 +334,13 @@ export const initialiseActionsEntreesPanier = (): void => {
)
.finally(() => {
// Réactive les Boutons
pipe(entrees, arrayMap(recupereElementsEntreePanier), majActivationBoutons(true));
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
.run();
});
},
)
.run();
.otherwise(_ => {});
});
})
);

View file

@ -2,22 +2,21 @@
* Scripts pour la mise à jour trans-fenêtres/trans-onglets du Bouton du Panier.
*/
import { pipe } from "@mobily/ts-belt";
import type { MessageMajBoutonPanier } from "./lib/types/messages";
import { ATTRIBUT_CONTIENT_ARTICLES, SELECTEUR_BOUTON_PANIER } from "./constantes/dom.ts";
import { NOM_CANAL_BOUTON_PANIER } from "./constantes/messages.ts";
import { recupereElementOuLeve } from "./lib/dom.ts";
import { mustGetEleInDocument } from "./lib/dom.ts";
import { valideMessageMajBoutonPanier } from "./lib/messages.ts";
import { recupereElementDocumentEither } from "./lib/utils.ts";
/**
* Initialise les interactions et la mise à jour du bouton « Panier » contenant le nombre d'articles dans le Panier.
*
* @returns void
*/
const initialiseBoutonPanier = (): void => {
/** Le « Bouton » vers le Panier dont le texte est un indicateur du nombre de Produits dedans. */
const BOUTON_PANIER: HTMLAnchorElement = pipe(
recupereElementDocumentEither<HTMLAnchorElement>(SELECTEUR_BOUTON_PANIER),
recupereElementOuLeve,
);
/** Le « Bouton » vers le Panier avec un indicateur de la quantité de Produits ajoutés. */
const BOUTON_PANIER: HTMLAnchorElement = mustGetEleInDocument<HTMLAnchorElement>(SELECTEUR_BOUTON_PANIER);
const CANAL_BOUTON_PANIER: BroadcastChannel = new BroadcastChannel(NOM_CANAL_BOUTON_PANIER);
CANAL_BOUTON_PANIER.onmessage = (evenementMessage: MessageEvent<unknown>): void => {
@ -35,6 +34,6 @@ const initialiseBoutonPanier = (): void => {
});
};
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", (): void => {
initialiseBoutonPanier();
});

View file

@ -10,22 +10,13 @@ import {
ATTRIBUT_TABINDEX,
SELECTEUR_BOUTON_MENU_MOBILE,
} from "./constantes/dom";
import { recupereEleOuLeve } from "./lib/utils";
/**
* 1. Récupérer la hauteur de l'écran et la hauteur de la page.
* 2. Si la page fait moins de 3 fois la hauteur de l'écran, arrêter.
* 3. Recommencer l'opération si la page est redimensionnée (ResizeObserver).
* 4. Quand l'Utilisateur dépasse le seuil de 3 fois la hauteur de l'écran, faire apparaître le bouton.
* 5. Le faire disparaître quand il retourne en-deça.
* 6. Au clic dessus, aller sur l'ancre au sommet de la page et faire disparaître le bouton.
*/
import { mustGetEleInDocument } from "./lib/dom";
const E = {
BOUTON_MENU_MOBILE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_MENU_MOBILE),
BOUTON_RETOUR_SOMMET: recupereEleOuLeve<HTMLButtonElement>("#bouton-retour-haut"),
CORPS_HTML: recupereEleOuLeve<HTMLBodyElement>("body"),
IMAGE_BOUTON: recupereEleOuLeve<HTMLImageElement>("#bouton-retour-haut img"),
BOUTON_MENU_MOBILE: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_MENU_MOBILE),
BOUTON_RETOUR_SOMMET: mustGetEleInDocument<HTMLButtonElement>("#bouton-retour-haut"),
CORPS_HTML: mustGetEleInDocument<HTMLBodyElement>("body"),
IMAGE_BOUTON: mustGetEleInDocument<HTMLImageElement>("#bouton-retour-haut img"),
};
/** Le ratio minimum hauteur de page/hauteur de la fenêtre à atteindre pour que le Bouton soit nécessaire. */
@ -47,6 +38,7 @@ const majDefilementY = (): number => window.scrollY;
/**
* @param estVisible
* @returns void
*/
const majVisibiliteBouton = (estVisible: boolean): void => {
if (estVisible) {

View file

@ -4,17 +4,17 @@ import { A } from "@mobily/ts-belt";
import { match } from "ts-pattern";
import { SELECTEUR_ENTREE_MENU_CATEGORIES_PRODUITS, SELECTEUR_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
import { recupereEleOuLeve, recupereElesOuLeve } from "./lib/utils.ts";
import { mustGetEleInDocument, mustGetElesInDocument } from "./lib/dom.ts";
document.addEventListener("DOMContentLoaded", (): void => {
const MENU_CATEGORIES_PRODUITS: HTMLElement = recupereEleOuLeve(SELECTEUR_MENU_CATEGORIES_PRODUITS);
const ENTREES_MENU_CATEGORIES_PRODUITS: Array<HTMLAnchorElement> = recupereElesOuLeve(
const MENU_CATEGORIES_PRODUITS: HTMLElement = mustGetEleInDocument(SELECTEUR_MENU_CATEGORIES_PRODUITS);
const ENTREES_MENU_CATEGORIES_PRODUITS: Array<HTMLAnchorElement> = mustGetElesInDocument(
SELECTEUR_ENTREE_MENU_CATEGORIES_PRODUITS,
);
A.forEachWithIndex(
[ENTREES_MENU_CATEGORIES_PRODUITS.at(0), ENTREES_MENU_CATEGORIES_PRODUITS.at(-1)],
(index, entreeMenu) => {
(index, entreeMenu): void => {
if (!entreeMenu) return;
new IntersectionObserver(

View file

@ -6,13 +6,13 @@ import { A, O, pipe } from "@mobily/ts-belt";
import A11yDialog from "a11y-dialog";
import { ATTRIBUT_MENU_MOBILE_ACTIVE, SELECTEUR_BOUTON_MENU_MOBILE, SELECTEUR_MENU_MOBILE } from "./constantes/dom.ts";
import { recupereEleOuLeve } from "./lib/utils.ts";
import { mustGetEleInDocument } from "./lib/dom.ts";
// Éléments d'intérêt
const E = {
BOUTON_MENU_MOBILE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_MENU_MOBILE),
CORPS_HTML: recupereEleOuLeve<HTMLBodyElement>("body"),
MENU_MOBILE: recupereEleOuLeve<HTMLDivElement>(SELECTEUR_MENU_MOBILE),
BOUTON_MENU_MOBILE: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_MENU_MOBILE),
CORPS_HTML: mustGetEleInDocument<HTMLBodyElement>("body"),
MENU_MOBILE: mustGetEleInDocument<HTMLDivElement>(SELECTEUR_MENU_MOBILE),
};
const initialiseBoutonMenuMobile = (): void => {

View file

@ -13,16 +13,16 @@ import {
SELECTEUR_CONTENEUR_STORYTELLING_A_PROPOS,
SELECTEUR_EPINGLE,
} from "./constantes/dom.ts";
import { recupereEleOuLeve, recupereElesOuLeve } from "./lib/utils.ts";
import { mustGetEleInDocument, mustGetElesInDocument } from "./lib/dom.ts";
/** Le Conteneur des images du storytelling. */
const CONTENEUR_STORYTELLING = recupereEleOuLeve<HTMLElement>(
const CONTENEUR_STORYTELLING = mustGetEleInDocument<HTMLElement>(
SELECTEUR_CONTENEUR_STORYTELLING_A_PROPOS,
);
/** */
const EPINGLES = recupereElesOuLeve<HTMLButtonElement>(SELECTEUR_EPINGLE);
const EPINGLES = mustGetElesInDocument<HTMLButtonElement>(SELECTEUR_EPINGLE);
/** */
const BOITES_TEXTE = recupereElesOuLeve<HTMLDivElement>(SELECTEUR_BOITE_TEXTE);
const BOITES_TEXTE = mustGetElesInDocument<HTMLDivElement>(SELECTEUR_BOITE_TEXTE);
/** */
const ENSEMBLES_EPINGLES_BOITES_TEXTE = new Map<string, [HTMLButtonElement, HTMLDivElement]>();
A.forEachWithIndex(EPINGLES, (index, epingle) => {

View file

@ -8,17 +8,17 @@ import {
SELECTEUR_CONTENEUR_STORYTELLING,
SELECTEUR_IMAGES_STORYTELLING,
} from "./constantes/dom.ts";
import { mustGetEleInDocument, mustGetElesInDocument } from "./lib/dom.ts";
import { estEntreDeuxNombres } from "./lib/nombres.ts";
import { recupereEleOuLeve, recupereElesOuLeve } from "./lib/utils.ts";
const initialiseScrollStorytelling = (): void => {
const E = {
/** Le conteneur des images du storytelling. */
CONTENEUR_STORYTELLING: recupereEleOuLeve<HTMLElement>(".storytelling__conteneur"),
CONTENEUR_STORYTELLING: mustGetEleInDocument<HTMLElement>(".storytelling__conteneur"),
/** Les images du storytelling. */
IMAGES_STORYTELLING: recupereElesOuLeve<HTMLDivElement>(SELECTEUR_IMAGES_STORYTELLING),
IMAGES_STORYTELLING: mustGetElesInDocument<HTMLDivElement>(SELECTEUR_IMAGES_STORYTELLING),
/** Le bloc contenant le storytelling. */
STORYTELLING: recupereEleOuLeve<HTMLElement>(SELECTEUR_CONTENEUR_STORYTELLING),
STORYTELLING: mustGetEleInDocument<HTMLElement>(SELECTEUR_CONTENEUR_STORYTELLING),
};
/** La hauteur d'une image du storytelling. */

View file

@ -10,7 +10,7 @@ import { ValiError } from "valibot";
import type { APIFetchErrors } from "./lib/types/api/erreurs";
import type { WCV3Products, WCV3ProductsArgs } from "./lib/types/api/v3/products.ts";
import type { EtatsPageGenerique } from "./lib/types/pages";
import type { GenericPageState } from "./lib/types/pages";
import { ROUTE_API_NOUVELLE_PRODUCTS } from "./constantes/api.ts";
import {
@ -23,48 +23,53 @@ import {
SELECTEUR_GRILLE_PRODUITS,
} from "./constantes/dom.ts";
import { lanceAnimationCycleLoading } from "./lib/animations.ts";
import { html } from "./lib/dom.ts";
import { html, mustGetEleInDocument } from "./lib/dom.ts";
import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts";
import { creeReponseSimplifiee, getBackendAvecParametresUrl } from "./lib/reseau.ts";
import { newPartialResponse, getBackendAvecParametresUrl } from "./lib/reseau.ts";
import { WCV3ProductsArgsSchema, WCV3ProductsSchema } from "./lib/schemas/api/v3/products.ts";
import { recupereEleOuLeve } from "./lib/utils.ts";
import { eitherValiParse } from "./lib/validation.ts";
import { safeSchemaParse } from "./lib/validation.ts";
type APIProductsErrors =
| APIFetchErrors
| ValiError<typeof WCV3ProductsArgsSchema>
| ValiError<typeof WCV3ProductsSchema>;
// @ts-expect-error -- États injectés par le modèle PHP
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPageGenerique = _etats;
const ETATS_PAGE: GenericPageState = _etats;
// Numéros magiques
const PRODUCTS_PER_PAGE = 12;
// Éléments d'intérêt
const E = {
BOUTON_PLUS_DE_PRODUITS: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_PLUS_PRODUITS),
GRILLE_PRODUITS: recupereEleOuLeve<HTMLDivElement>(SELECTEUR_GRILLE_PRODUITS),
BOUTON_PLUS_DE_PRODUITS: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_PLUS_PRODUITS),
GRILLE_PRODUITS: mustGetEleInDocument<HTMLDivElement>(SELECTEUR_GRILLE_PRODUITS),
};
/**
* TODO
*/
const initialisePageBoutique = (): void => {
/** ID de la Catégorie de Produits si la Page courante est l'Archive d'une Catégorie. */
const idCategorieProduits: null | string = E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS);
E.BOUTON_PLUS_DE_PRODUITS.addEventListener("click", (): void => {
/** Le numéro de page demandée par l'Utilisateur. */
const nouveauNumeroPage = Number(E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_PAGE)) + 1;
/** Les arguments passés à la requête auprès Backend pour la nouvelle page de Produits. */
const args: WCV3ProductsArgs = {
page: nouveauNumeroPage,
per_page: 12,
per_page: PRODUCTS_PER_PAGE,
// Ajoute conditionnellement la Catégorie de Produits
...(idCategorieProduits && { category: idCategorieProduits }),
};
type APIProductsErrors =
| APIFetchErrors
| ValiError<typeof WCV3ProductsArgsSchema>
| ValiError<typeof WCV3ProductsSchema>;
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(eitherValiParse(args, WCV3ProductsArgsSchema))
.liftEither(safeSchemaParse(args, WCV3ProductsArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement
.ifRight(() => {
.ifRight((): void => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
E.BOUTON_PLUS_DE_PRODUITS.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_PLUS_DE_PRODUITS.setAttribute(ATTRIBUT_CHARGEMENT, "");
@ -86,7 +91,7 @@ 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 creeReponseSimplifiee(reponse))
return 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)
@ -94,11 +99,11 @@ const initialisePageBoutique = (): void => {
})
)
// 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(eitherValiParse(corpsReponse, WCV3ProductsSchema)))
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCV3ProductsSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((donnees: WCV3Products) => {
// Cache le bouton s'il y a moins de 12 Produits disponibles (que l'on est à la dernière page)
if (donnees.length < 12) {
// Cache le bouton s'il y a moins de PRODUCTS_PER_PAGE Produits disponibles (que l'on est à la dernière page)
if (donnees.length < PRODUCTS_PER_PAGE) {
E.BOUTON_PLUS_DE_PRODUITS.toggleAttribute(ATTRIBUT_HIDDEN);
}
@ -106,7 +111,7 @@ const initialisePageBoutique = (): void => {
const fragment: DocumentFragment = document.createDocumentFragment();
// Créé les Éléments <article> à insérer
for (const produit of donnees.slice(0, 12)) {
for (const produit of donnees.slice(0, PRODUCTS_PER_PAGE)) {
pipe(
html`
<article class="produit">

View file

@ -2,7 +2,7 @@ import type { ValiError } from "valibot";
import { D, pipe } from "@mobily/ts-belt";
import { forEach as arrayForEach } from "@mobily/ts-belt/Array";
import { type Either, Maybe } from "purify-ts";
import { Maybe } from "purify-ts";
import type { MessageMajContenuPanierSchema } from "./lib/schemas/messages.ts";
import type { WCStoreCartItem } from "./lib/types/api/cart";
@ -14,32 +14,26 @@ import {
ATTRIBUT_DESACTIVE,
ATTRIBUT_HIDDEN,
SELECTEUR_BOUTON_ADDITION_QUANTITE,
SELECTEUR_BOUTON_SEPARATION_ADRESSES,
SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE,
SELECTEUR_BOUTON_SUPPRESSION_PANIER,
SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER,
SELECTEUR_CONTENEUR_PANIER,
SELECTEUR_ENTREES_PANIER,
SELECTEUR_FORMULAIRE_FACTURATION,
SELECTEUR_PRIX_LIGNE_PANIER,
SELECTEUR_SOUS_TOTAL_PRODUITS,
SELECTEUR_TOTAL_PANIER,
SELECTEUR_TOTAL_REDUCTION_VALEUR,
} from "./constantes/dom.ts";
import { NOM_CANAL_BOUTON_PANIER, NOM_CANAL_CONTENU_PANIER } from "./constantes/messages.ts";
import { recupereElementAvecSelecteur, recupereElementOuLeve, recupereElementsAvecSelecteur } from "./lib/dom.ts";
import { getDOMElementsWithSelector, recupereElementAvecSelecteur, recupereElementOuLeve } from "./lib/dom.ts";
import { type CleNonTrouveError, reporteErreur } from "./lib/erreurs.ts";
import { valideMessageMajBoutonPanier, valideMessageMajContenuPanier } from "./lib/messages.ts";
import { arrondisADeuxDecimales, diviseParCent, formateEnEuros, inverseNombre } from "./lib/nombres.ts";
import { propEither, recupereElementsDocumentEither, recupereEleOuLeve } from "./lib/utils.ts";
import { propEither } from "./lib/utils.ts";
import {
initialiseBoutonCalculLivraison,
initialiseBoutonCreationCommande,
initialiseEmetteursEvenementsFormulairePanier,
initCartFormEventEmiters,
initOrderCreationButton,
initShippingCalculationButton,
} from "./page-panier/scripts-page-panier-adresses.ts";
import { initialiseElementsCodePromo } from "./page-panier/scripts-page-panier-code-promo.ts";
import { E } from "./page-panier/scripts-page-panier-elements.ts";
import { souscrisEvenementsPanier } from "./page-panier/scripts-page-panier-evenement.ts";
import { initialiseBoutonsChoixMethodesLivraison } from "./page-panier/scripts-page-panier-methodes-livraison.ts";
import { initShippingRatesChoicesActions } from "./page-panier/scripts-page-panier-methodes-livraison.ts";
import { initialiseActionsEntreesPanier } from "./page-panier/scripts-page-panier-panneau-produits.ts";
type ElementsEntreePanier = {
@ -60,19 +54,6 @@ type EtatsPage = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPage = _etats;
// Éléments d'intérêt
const ENTREES_PANIER_EITHER: Either<SyntaxError, Array<HTMLElement>> = recupereElementsDocumentEither<HTMLElement>(
SELECTEUR_ENTREES_PANIER,
);
const CONTENEUR_PANIER: HTMLElement = recupereEleOuLeve(SELECTEUR_CONTENEUR_PANIER);
const SOUS_TOTAL_PRODUITS: HTMLElement = recupereEleOuLeve(SELECTEUR_SOUS_TOTAL_PRODUITS);
const SOUS_TOTAL_REDUCTION: HTMLSpanElement = recupereEleOuLeve(SELECTEUR_TOTAL_REDUCTION_VALEUR);
const TOTAL_PANIER: HTMLParagraphElement = recupereEleOuLeve(SELECTEUR_TOTAL_PANIER);
const BOUTON_SEPARATION_ADRESSES: HTMLInputElement = recupereEleOuLeve(
SELECTEUR_BOUTON_SEPARATION_ADRESSES,
);
const FORMULAIRE_FACTURATION: HTMLDivElement = recupereEleOuLeve(SELECTEUR_FORMULAIRE_FACTURATION);
/**
* Fonction utilitaire pour récupérer un Élément dans une ligne (entrée) du Panier, en levant une
* Erreur s'il n'existe pas.
@ -112,7 +93,7 @@ const initialiseMajConteneurPanier = (): void => {
.map(D.getUnsafe("donnees"))
// Met à jour l'Attribut de présence de Produits dans le Panier
.ifRight((donnees: MessageMajBoutonPanierDonnees) => {
CONTENEUR_PANIER.setAttribute(ATTRIBUT_CONTIENT_ARTICLES, String(donnees.quantiteProduits !== 0));
E.CONTENEUR_PANIER.setAttribute(ATTRIBUT_CONTIENT_ARTICLES, String(donnees.quantiteProduits !== 0));
});
};
};
@ -126,7 +107,7 @@ const initialiseMajContenuPanier = (): void => {
.ifRight((donnees: MessageMajContenuPanierDonnees) => {
donnees.produits.forEach((ligne: WCStoreCartItem) => {
// Met à jour les entrées du Panier
ENTREES_PANIER_EITHER.ifRight((entrees: Array<HTMLElement>) => {
E.ENTREES_PANIER.ifRight((entrees: Array<HTMLElement>) => {
Maybe
.fromNullable(entrees.find(entree => entree.getAttribute(ATTRIBUT_CLE_PANIER) === ligne.key))
.ifJust((entree: HTMLElement) => {
@ -149,41 +130,41 @@ const initialiseMajContenuPanier = (): void => {
});
// Met à jour les totaux du Panier
SOUS_TOTAL_PRODUITS.textContent = formateEnEuros(donnees.sousTotalProduits);
SOUS_TOTAL_REDUCTION.textContent = pipe(
E.SOUS_TOTAL_PRODUITS.textContent = formateEnEuros(donnees.sousTotalProduits);
E.SOUS_TOTAL_REDUCTION.textContent = pipe(
inverseNombre(donnees.sousTotalReduction),
arrondisADeuxDecimales,
formateEnEuros,
);
TOTAL_PANIER.textContent = pipe(arrondisADeuxDecimales(donnees.totalPanier), formateEnEuros);
E.TOTAL_PANIER.textContent = pipe(arrondisADeuxDecimales(donnees.totalPanier), formateEnEuros);
});
});
})
// Reporte tout Erreur et réactive les Boutons
.ifLeft((erreur: CleNonTrouveError | ValiError<typeof MessageMajContenuPanierSchema>) => {
reporteErreur(erreur);
ENTREES_PANIER_EITHER.ifRight(entrees => majEtatsActivationBoutons(entrees));
E.ENTREES_PANIER.ifRight(entrees => majEtatsActivationBoutons(entrees));
});
};
};
const initialiseMajFormulairesPanier = (): void => {
BOUTON_SEPARATION_ADRESSES.addEventListener("click", (): void => {
E.BOUTON_SEPARATION_ADRESSES.addEventListener("click", (): void => {
Maybe
.fromFalsy(BOUTON_SEPARATION_ADRESSES.checked)
.fromFalsy(E.BOUTON_SEPARATION_ADRESSES.checked)
// Les Adresses sont séparées
.ifJust((): void => {
// Rend visible le formulaire de facturation
FORMULAIRE_FACTURATION.removeAttribute(ATTRIBUT_HIDDEN);
recupereElementsAvecSelecteur(FORMULAIRE_FACTURATION)("input, select").ifRight(
E.FORMULAIRE_FACTURATION.removeAttribute(ATTRIBUT_HIDDEN);
getDOMElementsWithSelector(E.FORMULAIRE_FACTURATION)("input, select").ifRight(
arrayForEach(champ => champ.removeAttribute(ATTRIBUT_DESACTIVE)),
);
})
// Les Adresses sont combinées
.ifNothing((): void => {
// Cache le formulaire de facturation
FORMULAIRE_FACTURATION.setAttribute(ATTRIBUT_HIDDEN, "");
recupereElementsAvecSelecteur(FORMULAIRE_FACTURATION)<HTMLInputElement | HTMLSelectElement>(
E.FORMULAIRE_FACTURATION.setAttribute(ATTRIBUT_HIDDEN, "");
getDOMElementsWithSelector(E.FORMULAIRE_FACTURATION)<HTMLInputElement | HTMLSelectElement>(
"input, select",
).ifRight(arrayForEach(champ => {
champ.setAttribute(ATTRIBUT_DESACTIVE, "");
@ -194,14 +175,14 @@ const initialiseMajFormulairesPanier = (): void => {
};
document.addEventListener("DOMContentLoaded", (): void => {
initialiseEmetteursEvenementsFormulairePanier();
initCartFormEventEmiters();
souscrisEvenementsPanier();
initialiseActionsEntreesPanier();
initialiseBoutonsChoixMethodesLivraison();
initShippingRatesChoicesActions();
initialiseMajConteneurPanier();
initialiseMajContenuPanier();
initialiseMajFormulairesPanier();
initialiseBoutonCalculLivraison();
initialiseBoutonCreationCommande();
initShippingCalculationButton();
initOrderCreationButton();
initialiseElementsCodePromo();
});

View file

@ -33,14 +33,14 @@ import {
SELECTEUR_SELECTEUR_QUANTITE,
} from "./constantes/dom";
import { lanceAnimationCycleLoading } from "./lib/animations.ts";
import { recupereElementDocumentEither, mustGetEleInDocument, mustGetElesInDocument } from "./lib/dom.ts";
import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts";
import { estHTMLSelectElement } from "./lib/gardes.ts";
import { emetMessageMajBoutonPanier } from "./lib/messages.ts";
import { creeReponseSimplifiee, eitherAsyncFetch, postBackend } from "./lib/reseau.ts";
import { newPartialResponse, safeFetch, postBackend } from "./lib/reseau.ts";
import { WCStoreCartAddItemArgsSchema } from "./lib/schemas/api/cart-add-item.ts";
import { WCStoreCartSchema } from "./lib/schemas/api/cart.ts";
import { recupereElementDocumentEither, recupereEleOuLeve, recupereElesOuLeve } from "./lib/utils.ts";
import { eitherValiParse } from "./lib/validation.ts";
import { safeSchemaParse } from "./lib/validation.ts";
type EnsembleLienContenu = [HTMLAnchorElement, HTMLElement];
/** États utiles pour les scripts de la page. */
@ -67,18 +67,18 @@ const deplieToutesSections = (ensembleLiensContenus: Array<EnsembleLienContenu>)
// Éléments d'intérêt
const E = {
BOUTON_AJOUT_PANIER: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_BOUTON_AJOUT_PANIER),
CONTENEUR_FLECHES_DEFILEMENT: recupereEleOuLeve<HTMLDivElement>(SELECTEUR_CONTENEUR_FLECHES_DEFILEMENT),
FLECHE_DEFILEMENT_DROITE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_FLECHE_DEFILEMENT_DROITE),
FLECHE_DEFILEMENT_GAUCHE: recupereEleOuLeve<HTMLButtonElement>(SELECTEUR_FLECHE_DEFILEMENT_GAUCHE),
BOUTON_AJOUT_PANIER: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_BOUTON_AJOUT_PANIER),
CONTENEUR_FLECHES_DEFILEMENT: mustGetEleInDocument<HTMLDivElement>(SELECTEUR_CONTENEUR_FLECHES_DEFILEMENT),
FLECHE_DEFILEMENT_DROITE: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_FLECHE_DEFILEMENT_DROITE),
FLECHE_DEFILEMENT_GAUCHE: mustGetEleInDocument<HTMLButtonElement>(SELECTEUR_FLECHE_DEFILEMENT_GAUCHE),
IMAGES: A.flat([
recupereEleOuLeve<HTMLImageElement>(SELECTEUR_IMAGE_COLONNE_GAUCHE),
recupereElesOuLeve<HTMLImageElement>(SELECTEUR_IMAGES_COLONNE_DROITE),
mustGetEleInDocument<HTMLImageElement>(SELECTEUR_IMAGE_COLONNE_GAUCHE),
mustGetElesInDocument<HTMLImageElement>(SELECTEUR_IMAGES_COLONNE_DROITE),
]),
LIENS_ONGLETS: recupereElesOuLeve<HTMLAnchorElement>(SELECTEUR_LIENS_ONGLETS),
PHOTOS_PRODUIT: recupereEleOuLeve<HTMLElement>(SELECTEUR_PHOTOS_PRODUIT),
PRIX_PRODUIT: recupereEleOuLeve<HTMLParagraphElement>(SELECTEUR_PRIX_PRODUIT),
SECTIONS_CONTENUS: recupereElesOuLeve<HTMLElement>(SELECTEUR_SECTIONS_CONTENUS),
LIENS_ONGLETS: mustGetElesInDocument<HTMLAnchorElement>(SELECTEUR_LIENS_ONGLETS),
PHOTOS_PRODUIT: mustGetEleInDocument<HTMLElement>(SELECTEUR_PHOTOS_PRODUIT),
PRIX_PRODUIT: mustGetEleInDocument<HTMLParagraphElement>(SELECTEUR_PRIX_PRODUIT),
SECTIONS_CONTENUS: mustGetElesInDocument<HTMLElement>(SELECTEUR_SECTIONS_CONTENUS),
SELECTEUR_VARIATION: recupereElementDocumentEither<HTMLSelectElement>(SELECTEUR_SELECTEUR_QUANTITE),
};
@ -201,7 +201,7 @@ const ajouteProduitAuPanier = (): void => {
// Réalise la Requête et traite sa Réponse
void EitherAsync
// 1. Valide les arguments de la Requête
.liftEither(eitherValiParse(argsRequete, WCStoreCartAddItemArgsSchema))
.liftEither(safeSchemaParse(argsRequete, WCStoreCartAddItemArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement
.ifRight(() => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
@ -213,7 +213,7 @@ const ajouteProduitAuPanier = (): void => {
})
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartAddItemArgs) =>
eitherAsyncFetch(
safeFetch(
postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
@ -225,7 +225,7 @@ const ajouteProduitAuPanier = (): void => {
.chain((reponse: Response) =>
EitherAsync<BadRequestError | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
match(await creeReponseSimplifiee(reponse))
match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 201 }, r => r.body)
@ -233,7 +233,7 @@ const ajouteProduitAuPanier = (): void => {
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(eitherValiParse(corpsReponse, WCStoreCartSchema)))
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((panier: WCStoreCart) =>
pipe(